Files
solana/web3.js/src/budget-program.js

365 lines
8.7 KiB
JavaScript
Raw Normal View History

2018-09-18 12:46:59 -07:00
// @flow
import * as BufferLayout from 'buffer-layout';
2018-09-18 12:46:59 -07:00
import {Transaction} from './transaction';
2018-09-30 18:42:45 -07:00
import {PublicKey} from './publickey';
import * as Layout from './layout';
2018-09-18 12:46:59 -07:00
/**
* Represents a condition that is met by executing a `applySignature()`
* transaction
2018-09-20 20:20:37 -07:00
*
* @typedef {Object} SignatureCondition
* @property {string} type Must equal the string 'timestamp'
* @property {PublicKey} from Public key from which `applySignature()` will be accepted from
2018-09-18 12:46:59 -07:00
*/
export type SignatureCondition = {
2018-11-04 11:41:21 -08:00
type: 'signature',
from: PublicKey,
2018-09-18 12:46:59 -07:00
};
/**
* Represents a condition that is met by executing a `applyTimestamp()`
* transaction
2018-09-20 20:20:37 -07:00
*
2018-09-27 14:34:07 -06:00
* @typedef {Object} TimestampCondition
2018-09-20 20:20:37 -07:00
* @property {string} type Must equal the string 'timestamp'
* @property {PublicKey} from Public key from which `applyTimestamp()` will be accepted from
* @property {Date} when The timestamp that was observed
2018-09-18 12:46:59 -07:00
*/
2018-09-27 14:34:07 -06:00
export type TimestampCondition = {
2018-11-04 11:41:21 -08:00
type: 'timestamp',
from: PublicKey,
when: Date,
2018-09-18 12:46:59 -07:00
};
/**
* Represents a payment to a given public key
2018-09-20 20:20:37 -07:00
*
* @typedef {Object} Payment
2019-03-05 17:52:13 -08:00
* @property {number} amount Number of lamports
2018-09-20 20:20:37 -07:00
* @property {PublicKey} to Public key of the recipient
2018-09-18 12:46:59 -07:00
*/
export type Payment = {
2018-11-04 11:41:21 -08:00
amount: number,
to: PublicKey,
};
2018-09-18 12:46:59 -07:00
/**
2018-09-20 20:20:37 -07:00
* A condition that can unlock a payment
*
2018-09-27 14:34:07 -06:00
* @typedef {SignatureCondition|TimestampCondition} BudgetCondition
2018-09-18 12:46:59 -07:00
*/
2018-09-27 14:34:07 -06:00
export type BudgetCondition = SignatureCondition | TimestampCondition;
2018-09-18 12:46:59 -07:00
/**
* @private
*/
function serializePayment(payment: Payment): Buffer {
2018-09-30 18:42:45 -07:00
const toData = payment.to.toBuffer();
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(8 + toData.length);
data.writeUInt32LE(payment.amount, 0);
toData.copy(data, 8);
return data;
2018-09-18 12:46:59 -07:00
}
/**
* @private
*/
2018-11-04 11:41:21 -08:00
function serializeDate(when: Date): Buffer {
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(8 + 20);
data.writeUInt32LE(20, 0); // size of timestamp as u64
2018-09-18 12:46:59 -07:00
function iso(date) {
function pad(number) {
if (number < 10) {
return '0' + number;
}
return number;
}
2018-11-04 11:41:21 -08:00
return (
date.getUTCFullYear() +
'-' +
pad(date.getUTCMonth() + 1) +
'-' +
pad(date.getUTCDate()) +
'T' +
pad(date.getUTCHours()) +
':' +
pad(date.getUTCMinutes()) +
':' +
pad(date.getUTCSeconds()) +
'Z'
);
2018-09-18 12:46:59 -07:00
}
2019-03-14 13:27:47 -07:00
data.write(iso(when), 8);
return data;
2018-09-18 12:46:59 -07:00
}
/**
* @private
*/
function serializeCondition(condition: BudgetCondition) {
2018-11-04 11:41:21 -08:00
switch (condition.type) {
case 'timestamp': {
const date = serializeDate(condition.when);
const from = condition.from.toBuffer();
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(4 + date.length + from.length);
data.writeUInt32LE(0, 0); // Condition enum = Timestamp
date.copy(data, 4);
from.copy(data, 4 + date.length);
return data;
2018-11-04 11:41:21 -08:00
}
case 'signature': {
2019-03-14 13:27:47 -07:00
const dataLayout = BufferLayout.struct([
2018-11-04 11:41:21 -08:00
BufferLayout.u32('condition'),
Layout.publicKey('from'),
]);
const from = condition.from.toBuffer();
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(4 + from.length);
dataLayout.encode(
2018-11-04 11:41:21 -08:00
{
instruction: 1, // Signature
from,
},
2019-03-14 13:27:47 -07:00
data,
2018-11-04 11:41:21 -08:00
);
2019-03-14 13:27:47 -07:00
return data;
2018-11-04 11:41:21 -08:00
}
default:
throw new Error(`Unknown condition type: ${condition.type}`);
2018-09-18 12:46:59 -07:00
}
}
/**
2018-09-20 10:10:46 -07:00
* Factory class for transactions to interact with the Budget program
2018-09-18 12:46:59 -07:00
*/
2018-09-20 10:10:46 -07:00
export class BudgetProgram {
2018-09-18 12:46:59 -07:00
/**
2018-09-20 10:10:46 -07:00
* Public key that identifies the Budget program
2018-09-18 12:46:59 -07:00
*/
2018-09-20 10:10:46 -07:00
static get programId(): PublicKey {
2018-11-04 11:41:21 -08:00
return new PublicKey(
2019-04-25 17:43:19 -07:00
'Budget1111111111111111111111111111111111111',
2018-11-04 11:41:21 -08:00
);
2018-09-18 12:46:59 -07:00
}
2018-09-19 17:35:16 -07:00
/**
2018-09-20 10:10:46 -07:00
* The amount of space this program requires
2018-09-19 17:35:16 -07:00
*/
static get space(): number {
return 128;
}
2018-09-18 12:46:59 -07:00
/**
* Creates a timestamp condition
*/
2018-11-04 11:41:21 -08:00
static timestampCondition(from: PublicKey, when: Date): TimestampCondition {
2018-09-18 12:46:59 -07:00
return {
type: 'timestamp',
from,
when,
};
}
/**
* Creates a signature condition
*/
2018-11-04 11:41:21 -08:00
static signatureCondition(from: PublicKey): SignatureCondition {
2018-09-18 12:46:59 -07:00
return {
type: 'signature',
from,
};
}
/**
2019-03-05 17:52:13 -08:00
* Generates a transaction that transfers lamports once any of the conditions are met
2018-09-18 12:46:59 -07:00
*/
static pay(
from: PublicKey,
2018-09-26 08:45:33 -07:00
program: PublicKey,
2018-09-18 12:46:59 -07:00
to: PublicKey,
amount: number,
...conditions: Array<BudgetCondition>
): Transaction {
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(1024);
2018-09-18 12:46:59 -07:00
let pos = 0;
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(0, pos); // NewBudget instruction
2018-09-18 12:46:59 -07:00
pos += 4;
switch (conditions.length) {
2018-11-04 11:41:21 -08:00
case 0:
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(0, pos); // Budget enum = Pay
2018-11-04 11:41:21 -08:00
pos += 4;
{
const payment = serializePayment({amount, to});
2019-03-14 13:27:47 -07:00
payment.copy(data, pos);
2018-11-04 11:41:21 -08:00
pos += payment.length;
}
return new Transaction().add({
keys: [{pubkey: from, isSigner: true}, {pubkey: to, isSigner: false}],
2018-11-04 11:41:21 -08:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data: data.slice(0, pos),
2018-11-04 11:41:21 -08:00
});
case 1:
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(1, pos); // Budget enum = After
2018-11-04 11:41:21 -08:00
pos += 4;
{
const condition = conditions[0];
const conditionData = serializeCondition(condition);
2019-03-14 13:27:47 -07:00
conditionData.copy(data, pos);
2018-11-04 11:41:21 -08:00
pos += conditionData.length;
const paymentData = serializePayment({amount, to});
2019-03-14 13:27:47 -07:00
paymentData.copy(data, pos);
2018-11-04 11:41:21 -08:00
pos += paymentData.length;
}
return new Transaction().add({
keys: [
{pubkey: from, isSigner: true},
{pubkey: program, isSigner: false},
{pubkey: to, isSigner: false},
],
2018-11-04 11:41:21 -08:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data: data.slice(0, pos),
2018-11-04 11:41:21 -08:00
});
case 2:
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(2, pos); // Budget enum = Or
2018-11-04 11:41:21 -08:00
pos += 4;
for (let condition of conditions) {
const conditionData = serializeCondition(condition);
2019-03-14 13:27:47 -07:00
conditionData.copy(data, pos);
2018-11-04 11:41:21 -08:00
pos += conditionData.length;
const paymentData = serializePayment({amount, to});
2019-03-14 13:27:47 -07:00
paymentData.copy(data, pos);
2018-11-04 11:41:21 -08:00
pos += paymentData.length;
}
return new Transaction().add({
keys: [
{pubkey: from, isSigner: true},
{pubkey: program, isSigner: false},
{pubkey: to, isSigner: false},
],
2018-11-04 11:41:21 -08:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data: data.slice(0, pos),
2018-11-04 11:41:21 -08:00
});
default:
throw new Error(
`A maximum of two conditions are support: ${
conditions.length
} provided`,
);
2018-09-18 12:46:59 -07:00
}
}
2018-09-26 09:49:59 -07:00
/**
2019-03-05 17:52:13 -08:00
* Generates a transaction that transfers lamports once both conditions are met
2018-09-26 09:49:59 -07:00
*/
static payOnBoth(
from: PublicKey,
program: PublicKey,
to: PublicKey,
amount: number,
condition1: BudgetCondition,
condition2: BudgetCondition,
): Transaction {
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(1024);
2018-09-26 09:49:59 -07:00
let pos = 0;
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(0, pos); // NewBudget instruction
2018-09-26 09:49:59 -07:00
pos += 4;
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(3, pos); // Budget enum = And
2018-09-26 09:49:59 -07:00
pos += 4;
for (let condition of [condition1, condition2]) {
const conditionData = serializeCondition(condition);
2019-03-14 13:27:47 -07:00
conditionData.copy(data, pos);
2018-09-26 09:49:59 -07:00
pos += conditionData.length;
}
const paymentData = serializePayment({amount, to});
2019-03-14 13:27:47 -07:00
paymentData.copy(data, pos);
2018-09-26 09:49:59 -07:00
pos += paymentData.length;
return new Transaction().add({
keys: [
{pubkey: from, isSigner: true},
{pubkey: program, isSigner: false},
{pubkey: to, isSigner: false},
],
2018-09-26 09:49:59 -07:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data: data.slice(0, pos),
2018-09-26 09:49:59 -07:00
});
}
/**
* Generates a transaction that applies a timestamp, which could enable a
* pending payment to proceed.
*/
2018-11-04 11:41:21 -08:00
static applyTimestamp(
from: PublicKey,
program: PublicKey,
to: PublicKey,
when: Date,
): Transaction {
2018-09-18 12:46:59 -07:00
const whenData = serializeDate(when);
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(4 + whenData.length);
2018-09-18 12:46:59 -07:00
2019-03-14 13:27:47 -07:00
data.writeUInt32LE(1, 0); // ApplyTimestamp instruction
whenData.copy(data, 4);
2018-09-18 12:46:59 -07:00
return new Transaction().add({
keys: [
{pubkey: from, isSigner: true},
{pubkey: program, isSigner: false},
{pubkey: to, isSigner: false},
],
2018-09-20 10:10:46 -07:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data,
2018-09-18 12:46:59 -07:00
});
}
2018-09-26 09:49:59 -07:00
/**
* Generates a transaction that applies a signature, which could enable a
* pending payment to proceed.
*/
2018-11-04 11:41:21 -08:00
static applySignature(
from: PublicKey,
program: PublicKey,
to: PublicKey,
): Transaction {
2019-03-19 12:54:47 -07:00
const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]);
2019-03-14 13:27:47 -07:00
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 2, // ApplySignature instruction
},
2019-03-14 13:27:47 -07:00
data,
);
2018-09-18 12:46:59 -07:00
return new Transaction().add({
keys: [
{pubkey: from, isSigner: true},
{pubkey: program, isSigner: false},
{pubkey: to, isSigner: false},
],
2018-09-20 10:10:46 -07:00
programId: this.programId,
2019-03-14 13:27:47 -07:00
data,
2018-09-18 12:46:59 -07:00
});
}
}