diff --git a/web3.js/examples/budget.js b/web3.js/examples/budget-timestamp.js similarity index 100% rename from web3.js/examples/budget.js rename to web3.js/examples/budget-timestamp.js diff --git a/web3.js/examples/budget-two-approvers.js b/web3.js/examples/budget-two-approvers.js new file mode 100644 index 0000000000..e8e3d1baaa --- /dev/null +++ b/web3.js/examples/budget-two-approvers.js @@ -0,0 +1,146 @@ +/* + Example of using the Budget program to perform a payment authorized by two parties +*/ + +//eslint-disable-next-line import/no-commonjs +const solanaWeb3 = require('..'); +//const solanaWeb3 = require('@solana/web3.js'); + +const account1 = new solanaWeb3.Account(); +const account2 = new solanaWeb3.Account(); +const contractFunds = new solanaWeb3.Account(); +const contractState = new solanaWeb3.Account(); + +const approver1 = new solanaWeb3.Account(); +const approver2 = new solanaWeb3.Account(); + +let url; +url = 'http://localhost:8899'; +//url = 'http://testnet.solana.com:8899'; +const connection = new solanaWeb3.Connection(url); + +function showBalance() { + console.log(`\n== Account State`); + return Promise.all([ + connection.getBalance(account1.publicKey), + connection.getBalance(account2.publicKey), + connection.getBalance(contractFunds.publicKey), + connection.getBalance(contractState.publicKey), + ]).then(([fromBalance, toBalance, contractFundsBalance, contractStateBalance]) => { + console.log(`Account1: ${account1.publicKey} has a balance of ${fromBalance}`); + console.log(`Account2: ${account2.publicKey} has a balance of ${toBalance}`); + console.log(`Contract Funds: ${contractFunds.publicKey} has a balance of ${contractFundsBalance}`); + console.log(`Contract State: ${contractState.publicKey} has a balance of ${contractStateBalance}`); + }); +} + +function confirmTransaction(signature) { + console.log('Confirming transaction:', signature); + return connection.confirmTransaction(signature) + .then((confirmation) => { + if (!confirmation) { + throw new Error('Transaction was not confirmed'); + } + console.log('Transaction confirmed'); + }); +} + +function airDrop() { + console.log(`\n== Requesting airdrop of 100 to ${account1.publicKey}`); + return connection.requestAirdrop(account1.publicKey, 100) + .then(confirmTransaction); +} + +showBalance() +.then(airDrop) +.then(() => { + console.log(`\n== Move 1 token to approver1`); + const transaction = solanaWeb3.SystemProgram.move( + account1.publicKey, + approver1.publicKey, + 1, + ); + return connection.sendTransaction(account1, transaction); +}) +.then(confirmTransaction) +.then(() => { + console.log(`\n== Move 1 token to approver2`); + const transaction = solanaWeb3.SystemProgram.move( + account1.publicKey, + approver2.publicKey, + 1, + ); + return connection.sendTransaction(account1, transaction); +}) +.then(confirmTransaction) +.then(showBalance) +.then(() => { + console.log(`\n== Creating account for the contract funds`); + const transaction = solanaWeb3.SystemProgram.createAccount( + account1.publicKey, + contractFunds.publicKey, + 50, // number of tokens to transfer + 0, + solanaWeb3.BudgetProgram.programId, + ); + return connection.sendTransaction(account1, transaction); +}) +.then(confirmTransaction) +.then(showBalance) +.then(() => { + console.log(`\n== Creating account for the contract state`); + const transaction = solanaWeb3.SystemProgram.createAccount( + account1.publicKey, + contractState.publicKey, + 1, // account1 pays 1 token to hold the contract state + solanaWeb3.BudgetProgram.space, + solanaWeb3.BudgetProgram.programId, + ); + return connection.sendTransaction(account1, transaction); +}) +.then(confirmTransaction) +.then(showBalance) +.then(() => { + console.log(`\n== Initializing contract`); + const transaction = solanaWeb3.BudgetProgram.payOnBoth( + contractFunds.publicKey, + contractState.publicKey, + account2.publicKey, + 50, + solanaWeb3.BudgetProgram.signatureCondition(approver1.publicKey), + solanaWeb3.BudgetProgram.signatureCondition(approver2.publicKey), + ); + return connection.sendTransaction(contractFunds, transaction); +}) +.then(confirmTransaction) +.then(showBalance) +.then(() => { + console.log(`\n== Apply approver 1`); + const transaction = solanaWeb3.BudgetProgram.applySignature( + approver1.publicKey, + contractState.publicKey, + account2.publicKey, + ); + return connection.sendTransaction(approver1, transaction); +}) +.then(confirmTransaction) +.then(showBalance) +.then(() => { + console.log(`\n== Apply approver 2`); + const transaction = solanaWeb3.BudgetProgram.applySignature( + approver2.publicKey, + contractState.publicKey, + account2.publicKey, + ); + return connection.sendTransaction(approver2, transaction); +}) +.then(confirmTransaction) +.then(showBalance) + +.then(() => { + console.log('\nDone'); +}) + +.catch((err) => { + console.log(err); +}); diff --git a/web3.js/src/budget-program.js b/web3.js/src/budget-program.js index dbbf4cceab..d8a9530f20 100644 --- a/web3.js/src/budget-program.js +++ b/web3.js/src/budget-program.js @@ -163,8 +163,7 @@ export class BudgetProgram { } /** - * Generates a transaction that transfer tokens once a set of conditions are - * met + * Generates a transaction that transfers tokens once any of the conditions are met */ static pay( from: PublicKey, @@ -173,7 +172,6 @@ export class BudgetProgram { amount: number, ...conditions: Array ): Transaction { - const userdata = Buffer.alloc(1024); let pos = 0; userdata.writeUInt32LE(0, pos); // NewContract instruction @@ -222,7 +220,7 @@ export class BudgetProgram { }); case 2: - userdata.writeUInt32LE(2, pos); // Budget enum = Ok + userdata.writeUInt32LE(2, pos); // Budget enum = Or pos += 4; for (let condition of conditions) { @@ -247,6 +245,51 @@ export class BudgetProgram { } } + /** + * Generates a transaction that transfers tokens once both conditions are met + */ + static payOnBoth( + from: PublicKey, + program: PublicKey, + to: PublicKey, + amount: number, + condition1: BudgetCondition, + condition2: BudgetCondition, + ): Transaction { + const userdata = Buffer.alloc(1024); + let pos = 0; + userdata.writeUInt32LE(0, pos); // NewContract instruction + pos += 4; + + userdata.writeUInt32LE(amount, pos); // Contract.tokens + pos += 8; + + userdata.writeUInt32LE(3, pos); // Budget enum = And + pos += 4; + + for (let condition of [condition1, condition2]) { + const conditionData = serializeCondition(condition); + conditionData.copy(userdata, pos); + pos += conditionData.length; + } + + const paymentData = serializePayment({amount, to}); + paymentData.copy(userdata, pos); + pos += paymentData.length; + + return new Transaction({ + fee: 0, + keys: [from, program, to], + programId: this.programId, + userdata: userdata.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 userdata = Buffer.alloc(4 + whenData.length); @@ -262,6 +305,10 @@ export class BudgetProgram { }); } + /** + * 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 userdata = Buffer.alloc(4); userdata.writeUInt32LE(2, 0); // ApplySignature instruction diff --git a/web3.js/test/budget-program.test.js b/web3.js/test/budget-program.test.js index f17b3d2b3e..e17eb2a8ef 100644 --- a/web3.js/test/budget-program.test.js +++ b/web3.js/test/budget-program.test.js @@ -38,6 +38,17 @@ test('pay', () => { ); expect(transaction.keys).toHaveLength(3); // TODO: Validate transaction contents more + + transaction = BudgetProgram.payOnBoth( + from.publicKey, + program.publicKey, + to.publicKey, + 123, + BudgetProgram.signatureCondition(from.publicKey), + BudgetProgram.timestampCondition(from.publicKey, new Date()), + ); + expect(transaction.keys).toHaveLength(3); + // TODO: Validate transaction contents more }); test('apply', () => {