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