diff --git a/docs/guides/paying-for-tx.md b/docs/guides/paying-for-tx.md new file mode 100644 index 0000000000..c4acaf44e0 --- /dev/null +++ b/docs/guides/paying-for-tx.md @@ -0,0 +1,27 @@ +# PayingForTx + +## Introduction +This guide explains you how to perform a `PayingForTx` (also known as meta-transaction) using the SDK. + +It is a very powerful and efficient solution that is crucial for onboarding new users into you ecosystem. By making use of the `PayingForTx` you will be able to cover the fees of your users. + +## How it works +Typically somebody that you want to pay the transaction for (e.g. a new user of your decentralized aepp) signs the **inner transaction** (e.g. of type `ContractCallTx`) with a **specific signature** that is used for inner transactions. + +You can then collect the signed inner transaction, wrap it into a `PayingForTx` and broadcast it to the network. + +## Usage examples +We provided following two NodeJS examples which you can take a look at: + +- [InnerTx: ContractCallTx](../examples/node/paying-for-tx-contract-call-tx.md) +- [InnerTx: SpendTx](../examples/node/paying-for-tx-spend-tx.md) + +Note: + +- A `PayingForTx` can wrap ***any kind*** of other [transaction type](https://github.com/aeternity/protocol/blob/master/consensus/consensus.md#transactions-1) supported by the protocol as inner transaction. + +## UseCases +- Game developers that want to quickly onboard new users. +- Governance aepps that want people to vote on important proposals without having them to pay anything. +- Custodians that want to offer an additional services to cover the transaction fees of their clients. +- ... many more! \ No newline at end of file diff --git a/docs/transaction-options.md b/docs/transaction-options.md index 5b574c1af4..bb0c69b32c 100644 --- a/docs/transaction-options.md +++ b/docs/transaction-options.md @@ -30,6 +30,8 @@ These options are common and can be provided to every tx-type: - `fee` (default: calculated for each tx-type) - The minimum fee is dependent on the tx-type. - You can provide a higher fee to additionally reward the miners. +- `innerTx` (default: `false`) + - Should be used for signing an inner transaction that will be wrapped in a `PayingForTx`. - `verify` (default: `false`) - If set to true the transaction will be verified prior to broadcasting it. - `waitMined` (default: `true`) diff --git a/examples/README.md b/examples/README.md index ae9fb961f0..872b3fee63 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,3 +15,4 @@ Create a [new issue](https://github.com/aeternity/aepp-sdk-js/issues/new) to sug ## NodeJS 1. [Contract interaction](node/contract-interaction.js) 2. [Transfer AE Tokens](node/transfer-ae-tokens.js) +3. [PayingForTx](node/paying-for-tx.js) diff --git a/examples/node/contract-interaction.js b/examples/node/contract-interaction.js index 9d318a3da8..d4fd54c113 100755 --- a/examples/node/contract-interaction.js +++ b/examples/node/contract-interaction.js @@ -20,20 +20,20 @@ // ## Introduction // The whole script is [located in the repository](https://github.com/aeternity/aepp-sdk-js/blob/master/examples/node/contract.js) and this page explains in detail how to: // -// * deal with the different phases of compiling Sophia contracts to bytecode -// * deploy the bytecode to get a callable contract address -// * invoke the deployed contract on the æternity blockchain using `callStatic` -// * get the contract instance of an already deployed contract -// * access an `entrypoint` (public contract function) directly without using `call` or `callStatic` +// - deal with the different phases of compiling Sophia contracts to bytecode +// - deploy the bytecode to get a callable contract address +// - invoke the deployed contract on the æternity blockchain using `callStatic` +// - get the contract instance of an already deployed contract +// - access an `entrypoint` (public contract function) directly without using `call` or `callStatic` // ## 1. Specify imports // -// You will need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. +// You need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. const { Universal, Node, MemoryAccount } = require('@aeternity/aepp-sdk') // **Note**: - -// - you need to have the SDK installed via `npm i @aetenity/aepp-sdk -g` to run that example code +// +// - You need to have the SDK installed via `npm i @aetenity/aepp-sdk -g` to run that example code. // ## 2. Define constants // The following constants are used in the subsequent code snippets. diff --git a/examples/node/paying-for-tx-contract-call-tx.js b/examples/node/paying-for-tx-contract-call-tx.js new file mode 100644 index 0000000000..6cde9abf43 --- /dev/null +++ b/examples/node/paying-for-tx-contract-call-tx.js @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/* + * ISC License (ISC) + * Copyright (c) 2021 aeternity developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// # InnerTx: ContractCallTx +// +// ## Introduction +// The whole script is [located in the repository](https://github.com/aeternity/aepp-sdk-js/blob/master/examples/node/paying-for-tx-contract-call-tx.js) +// and this page explains in detail how to: +// +// - Create and sign a `ContractCallTx` with the `innerTx` option for an account that has no balance. +// - Wrap the signed `ContractCallTx` in a `PayingForTx` using an account with balance to pay the fees of the inner transaction. +// +// Note: +// +// - This can be done for ***any*** transaction type! +// +// ### UseCases +// This functionality allows **every service** to let their users interact with their +// decentralized aepp without having them to buy AE tokens by covering their fees. +// +// Examples: +// +// - Game developers that want to quickly onboard new users. +// - Governance aepps that want people to vote on important proposals without having them to pay anything. +// - Custodians that want to offer an additional services to cover the transaction fees of their clients. +// - ... many more! + +// ## 1. Specify imports +// You need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. +// Additionally you import the `Crypto` utility module to generate a new keypair. +const { Universal, Node, MemoryAccount, Crypto } = require('@aeternity/aepp-sdk') + +// **Note**: +// +// - You need to have the SDK installed via `npm i @aetenity/aepp-sdk -g` to run that example code. + +// ## 2. Define constants +// The following constants are used in the subsequent code snippets. +const PAYER_ACCOUNT_KEYPAIR = { + publicKey: 'ak_2dATVcZ9KJU5a8hdsVtTv21pYiGWiPbmVcU1Pz72FFqpk9pSRR', + secretKey: 'bf66e1c256931870908a649572ed0257876bb84e3cdf71efb12f56c7335fad54d5cf08400e988222f26eb4b02c8f89077457467211a6e6d955edb70749c6a33b' +} +const NODE_URL = 'https://testnet.aeternity.io'; +const COMPILER_URL = 'https://compiler.aepps.com'; +const CONTRACT_ADDRESS = 'ct_iy86kak8GGt4U5VjDFNQf1a9qjbyxKpmGVNe3UuKwnmcM6LW8'; +const CONTRACT_SOURCE = +` +@compiler >= 6 + +contract PayingForTxExample = + + record state = { last_caller: option(address) } + + entrypoint init() = + { last_caller = None } + + stateful entrypoint set_last_caller() = + put(state{last_caller = Some(Call.caller)}) + + entrypoint get_last_caller() : option(address) = + state.last_caller +` +const NEW_USER_KEYPAIR = Crypto.generateKeyPair(); + +// Note: +// +// - The keypair of the account is pre-funded and only used for demonstration purpose +// - You can replace it with your own keypair (see [Create a Keypair](../../quick-start.md#2-create-a-keypair)) +// - In case the account runs out of funds you can request new AE tokens using the [Faucet](https://faucet.aepps.com/) +// - The contract is already deployed at the defined address. +// - The `NEW_USER_KEYPAIR` is used to call the contract. The `PayingForTx` allows the new user to perform a contract call without having any funds. + +// ## 3. Open async codeblock +// Most functions of the SDK return _Promises_, so the recommended way of +// dealing with subsequent actions is running them one by one using `await`. +// Therefore we are putting our logic into an `async` code block +(async () => { + + // ## 4. Create object instances + const payerAccount = MemoryAccount({ keypair: PAYER_ACCOUNT_KEYPAIR }) + const newUserAccount = MemoryAccount({ keypair: NEW_USER_KEYPAIR }) + const node = await Node({ url: NODE_URL }) + const client = await Universal({ + nodes: [{ name: 'testnet', instance: node }], + compilerUrl: COMPILER_URL, + accounts: [payerAccount, newUserAccount], + }) + + // The `Universal` [Stamp](https://stampit.js.org/essentials/what-is-a-stamp) itself is asynchronous as it determines the node's version and + // rest interface automatically. Only once the Promise is fulfilled, you know you have a working object instance + // which is assigned to the `client` constant in this case. + // + // Note: + // + // - `Universal` is not a constructor but a factory, which means it's *not* invoked with `new`. + + // ## 5. Create and sign `ContractCallTx` on behalf of new user + // Currently there is no high-level API available that allows you to create and sign the `ContractCallTx` + // by invoking the generated contract method on the contract instance that you typically use for contract calls. + // + // Following 3 steps need to be done: + // + // 1. Create calldata by calling the http compiler using `contractEncodeCallDataAPI` and providing + // the contract source, the name of the `entrypoint` to call as well as the required params. + // - The `entrypoint` with the name `set_latest_caller` doesn't require any params so you can provide an empty array + // 1. Create the `ContractCreateTx` by providing all required params. + // - You could omit `amount`, `gas` and `gasPrice` if you choose to stick to the default values (see [transaction options](../transaction-options.md#contractcreatetx-contractcalltx)) + // 1. Sign the transaction by providing `innerTx: true` as transaction option. + // - The transaction will be signed in a special way that is required for inner transactions. + // + const calldata = await client.contractEncodeCallDataAPI(CONTRACT_SOURCE, "set_last_caller", []) + const contractCallTx = await client.contractCallTx({ callerId: await newUserAccount.address(), contractId: CONTRACT_ADDRESS, amount: 0, gas: 1000000, gasPrice: 1500000000, callData: calldata }) + const signedContractCallTx = await client.signTransaction(contractCallTx, { onAccount: newUserAccount, innerTx: true }) + + // ## 6. Create, sign & broadcast the `PayingForTx` as payer + const payForTx = await client.payForTransaction(signedContractCallTx, { onAccount: payerAccount }) + console.log(payForTx) + + // ## 7. Check that last caller is the new user + // Knowing the contract address and the source code allows you to + // initialize a contract instance and interact with the contract in a convenient way. + const contractInstance = await client.getContractInstance(CONTRACT_SOURCE, { contractAddress: CONTRACT_ADDRESS }) + const dryRunTx = await contractInstance.methods.get_last_caller() + console.log(`New user: ${await newUserAccount.address()}`) + console.log(`Last caller: ${dryRunTx.decodedResult}`) + + // Note: + // + // - Last caller should now be the address of the new user. + // - For regular (non-stateful) entrypoints the SDK automatically performs a dry-run which allows to perform read-only calls for free, see [Contract guide](../../guides/contracts.md#b-regular-entrypoints). + +// ## 8. Close and run async codeblock +// Now you can close the async codeblock and execute it at the same time. +})() diff --git a/examples/node/paying-for-tx-spend-tx.js b/examples/node/paying-for-tx-spend-tx.js new file mode 100644 index 0000000000..e75d0fadc6 --- /dev/null +++ b/examples/node/paying-for-tx-spend-tx.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/* + * ISC License (ISC) + * Copyright (c) 2021 aeternity developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// # InnerTx: SpendTx +// +// ## Introduction +// The whole script is [located in the repository](https://github.com/aeternity/aepp-sdk-js/blob/master/examples/node/paying-for-tx-spend-tx.js) +// and this page explains in detail how to: +// +// - Create and sign a `SpendTx` for an account with the `innerTx` option. +// - Wrap the signed `SpendTx` in a `PayingForTx`, signing it using an account that pays the fees of the inner `SpendTx` and broadcasts it to the network. +// +// Note: +// +// - This can be done for ***any*** transaction type! + +// ## 1. Specify imports +// You need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. +// Additionally you import the `Crypto` utility module to generate a new keypair. +const { Universal, Node, MemoryAccount, Crypto } = require('@aeternity/aepp-sdk') + +// **Note**: +// +// - You need to have the SDK installed via `npm i @aetenity/aepp-sdk -g` to run that example code. + +// ## 2. Define constants +// The following constants are used in the subsequent code snippets. +const PAYER_ACCOUNT_KEYPAIR = { + publicKey: 'ak_2dATVcZ9KJU5a8hdsVtTv21pYiGWiPbmVcU1Pz72FFqpk9pSRR', + secretKey: 'bf66e1c256931870908a649572ed0257876bb84e3cdf71efb12f56c7335fad54d5cf08400e988222f26eb4b02c8f89077457467211a6e6d955edb70749c6a33b' +} +const NODE_URL = 'https://testnet.aeternity.io' +const NEW_USER_KEYPAIR = Crypto.generateKeyPair() +const AMOUNT = 1; + +// Note: +// +// - The keypair of the account is pre-funded and only used for demonstration purpose +// - You can replace it with your own keypair (see [Create a Keypair](../../quick-start.md#2-create-a-keypair)) +// - In case the account runs out of funds you can request new AE tokens using the [Faucet](https://faucet.aepps.com/) +// - The `AMOUNT` (in `aettos`) will be send to the new user and returned to the payer. + +// ## 3. Open async codeblock +// Most functions of the SDK return _Promises_, so the recommended way of +// dealing with subsequent actions is running them one by one using `await`. +// Therefore we are putting our logic into an `async` code block +(async () => { + + // ## 4. Create object instances + const payerAccount = MemoryAccount({ keypair: PAYER_ACCOUNT_KEYPAIR }) + const newUserAccount = MemoryAccount({ keypair: NEW_USER_KEYPAIR }) + const node = await Node({ url: NODE_URL }) + const client = await Universal({ + nodes: [{ name: 'testnet', instance: node }], + accounts: [payerAccount, newUserAccount], + }) + + // The `Universal` [Stamp](https://stampit.js.org/essentials/what-is-a-stamp) itself is asynchronous as it determines the node's version and + // rest interface automatically. Only once the Promise is fulfilled, you know you have a working object instance + // which is assigned to the `client` constant in this case. + // + // Note: + // + // - `Universal` is not a constructor but a factory, which means it's *not* invoked with `new`. + + // ## 5. Send 1 `aetto` from payer to new user + const spendTxResult = await client.spend(AMOUNT, await newUserAccount.address(), { onAccount: payerAccount }) + console.log(spendTxResult) + + // ## 6. Check balance of new user (before) + const newUserBalanceBefore = await client.getBalance(await newUserAccount.address()) + console.log(`new user balance (before): ${newUserBalanceBefore}`) + + // Note: + // + // - The balance should now be 1 + + // ## 7. Create and sign `SpendTx` on behalf of new user + const spendTx = await client.spendTx({ + senderId: await newUserAccount.address(), + recipientId: await payerAccount.address(), + amount: AMOUNT + }) + const signedSpendTx = await client.signTransaction(spendTx, { onAccount: newUserAccount, innerTx: true }) + + // Note: + // + // - The provided [transaction option](../../transaction-options.md) `innerTx` indicates that the transaction needs + // to be signed in a special way + + // ## 7. Create, sign & broadcast the `PayingForTx` as payer + const payForTx = await client.payForTransaction(signedSpendTx, { onAccount: payerAccount }) + console.log(payForTx) + + // Note: + // + // - Normally sending the whole balance (1 `aetto`) would not be possible as the new user would have to cover the transaction fee. + + // ## 8. Check balance of new user (after) + const newUserBalanceAfter = await client.getBalance(await newUserAccount.address()) + console.log(`new user balance (after): ${newUserBalanceAfter}`) + + // Note: + // + // - The balance should now be 0 + +// ## 9. Close and run async codeblock +// Now you can close the async codeblock and execute it at the same time. +})() diff --git a/examples/node/transfer-ae-tokens.js b/examples/node/transfer-ae-tokens.js index 5fd6ca0e0a..f78d5b5b25 100755 --- a/examples/node/transfer-ae-tokens.js +++ b/examples/node/transfer-ae-tokens.js @@ -26,9 +26,13 @@ // - transfer AE tokens to another account // ## 1. Specify imports -// We'll need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. +// You need to import `Universal`, `Node` and `MemoryAccount` [Stamps](https://stampit.js.org/essentials/what-is-a-stamp) from the SDK. const { Universal, Node, MemoryAccount } = require('@aeternity/aepp-sdk') +// **Note**: +// +// - You need to have the SDK installed via `npm i @aetenity/aepp-sdk -g` to run that example code. + // ## 2. Define constants // The following constants are used in the subsequent code snippets. const ACCOUNT_KEYPAIR = { diff --git a/mkdocs.yml b/mkdocs.yml index c93f0480e7..775c728d8e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,7 @@ nav: - guides/contracts.md - guides/contract-events.md - guides/oracles.md + - guides/paying-for-tx.md - guides/low-vs-high-usage.md - Wallet interaction: - guides/connect-aepp-to-wallet.md @@ -60,6 +61,9 @@ nav: - NodeJS: - examples/node/transfer-ae-tokens.md - examples/node/contract-interaction.md + - PayingForTx: + - examples/node/paying-for-tx-contract-call-tx.md + - examples/node/paying-for-tx-spend-tx.md - tutorials/vuejs/helloworld-blockheight.md - api-reference.md - Changelog: CHANGELOG.md diff --git a/tooling/docs/examples-to-md.js b/tooling/docs/examples-to-md.js index a9fa3e1ab8..4385cc35af 100644 --- a/tooling/docs/examples-to-md.js +++ b/tooling/docs/examples-to-md.js @@ -40,7 +40,7 @@ process.argv.slice(2).forEach(fileName => { const textMd = splitCodeIntoBlocks(text) .map(({ type, content }) => ({ type, - content: type === 'code' ? content.replace(/^\n+|\n+$/g, '') : content.trimLeft() + content: type === 'code' ? content.replace(/^\n+|\n+$/g, '') : content.replace(/^ /, '') })) .filter(({ type, content }) => type !== 'code' || content) .filter(({ content }) => !content.includes('License'))