Skip to content

Commit

Permalink
Merge pull request #1248 from aeternity/docs/payingfortx-guide-and-ex…
Browse files Browse the repository at this point in the history
…amples

Docs/payingfortx guide and examples
  • Loading branch information
mradkov authored Jul 6, 2021
2 parents 81d031d + 37952cf commit ff8b56b
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 10 deletions.
27 changes: 27 additions & 0 deletions docs/guides/paying-for-tx.md
Original file line number Diff line number Diff line change
@@ -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!
2 changes: 2 additions & 0 deletions docs/transaction-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
16 changes: 8 additions & 8 deletions examples/node/contract-interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
149 changes: 149 additions & 0 deletions examples/node/paying-for-tx-contract-call-tx.js
Original file line number Diff line number Diff line change
@@ -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.
})()
124 changes: 124 additions & 0 deletions examples/node/paying-for-tx-spend-tx.js
Original file line number Diff line number Diff line change
@@ -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.
})()
6 changes: 5 additions & 1 deletion examples/node/transfer-ae-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tooling/docs/examples-to-md.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down

0 comments on commit ff8b56b

Please sign in to comment.