Skip to content

Commit

Permalink
feat: payForTransaction method
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk authored and mradkov committed Jul 5, 2021
1 parent 5bc7ab3 commit fbf204d
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 51 deletions.
8 changes: 5 additions & 3 deletions src/account/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ export const isAccountBase = (acc) => !['sign', 'address'].find(f => typeof acc[
* @rtype (tx: String) => tx: Promise[String], throws: Error
* @param {String} tx - Transaction to sign
* @param {Object} opt - Options
* @param {Object} [opt.innerTx] - Sign as inner transaction for PayingFor
* @return {String} Signed transaction
*/
async function signTransaction (tx, opt) {
const networkId = this.getNetworkId(opt)
async function signTransaction (tx, opt = {}) {
const prefixes = [this.getNetworkId(opt)]
if (opt.innerTx) prefixes.push('inner_tx')
const rlpBinaryTx = decode(tx, 'tx')
const txWithNetworkId = Buffer.concat([Buffer.from(networkId), hash(rlpBinaryTx)])
const txWithNetworkId = Buffer.concat([Buffer.from(prefixes.join('-')), hash(rlpBinaryTx)])

const signatures = [await this.sign(txWithNetworkId, opt)]
return buildTx({ encodedTx: rlpBinaryTx, signatures }, TX_TYPE.signed).tx
Expand Down
23 changes: 22 additions & 1 deletion src/ae/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ async function transferFunds (fraction, recipientIdOrName, options) {
return this.send(await this.spendTx({ ...opt, senderId, recipientId, amount }), opt)
}

/**
* Submit transaction of another account paying for it (fee and gas)
* @instance
* @category async
* @rtype (transaction: String, options?: Object) => Promise[String]
* @param {String} transaction - tx_<base64>-encoded transaction
* @param {Object} [options]
* @return {Object} Transaction
*/
async function payForTransaction (transaction, options) {
const opt = { ...this.Ae.defaults, ...options }
return this.send(
await this.payingForTx({
...opt,
payerId: await this.address(opt),
tx: transaction
}),
opt
)
}

/**
* Remove all listeners for RPC
* @instance
Expand Down Expand Up @@ -135,7 +156,7 @@ function destroyInstance () {
* @return {Object} Ae instance
*/
const Ae = stampit(Tx, AccountBase, Chain, {
methods: { send, spend, transferFunds, destroyInstance, signUsingGA },
methods: { send, spend, transferFunds, payForTransaction, destroyInstance, signUsingGA },
deepProps: { Ae: { defaults: { denomination: AE_AMOUNT_FORMATS.AETTOS } } }
})

Expand Down
1 change: 0 additions & 1 deletion src/contract/aci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export default async function getContractInstance (source, { aci, contractAddres
skipArgsConvert: false,
skipTransformDecoded: false,
callStatic: false,
waitMined: true,
filesystem
}
const instance = {
Expand Down
5 changes: 1 addition & 4 deletions src/contract/ga/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import BigNumber from 'bignumber.js'
import { MAX_AUTH_FUN_GAS, TX_TYPE } from '../../tx/builder/schema'
import { buildTx } from '../../tx/builder'
import { MAX_AUTH_FUN_GAS } from '../../tx/builder/schema'
import { hash } from '../../utils/crypto'

export const prepareGaParams = (ins) => async (authData, authFnName) => {
Expand All @@ -21,5 +20,3 @@ export const getContractAuthFan = (ins) => async (source, fnName) => {

return { bytecode, authFun: hash(fnName) }
}

export const wrapInEmptySignedTx = (rlp) => buildTx({ encodedTx: rlp, signatures: [] }, TX_TYPE.signed)
26 changes: 10 additions & 16 deletions src/contract/ga/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ import * as R from 'ramda'

import { ContractAPI } from '../../ae/contract'
import { TX_TYPE } from '../../tx/builder/schema'
import { buildTx } from '../../tx/builder'
import { decode } from '../../tx/builder/helpers'
import { getContractAuthFan, prepareGaParams, wrapInEmptySignedTx } from './helpers'
import { buildTx, unpackTx } from '../../tx/builder'
import { getContractAuthFan, prepareGaParams } from './helpers'

/**
* GeneralizeAccount Stamp
Expand Down Expand Up @@ -101,6 +100,8 @@ async function createGeneralizeAccount (authFnName, source, args = [], options =
})
}

const wrapInEmptySignedTx = (tx) => buildTx({ encodedTx: tx, signatures: [] }, TX_TYPE.signed)

/**
* Create a metaTx transaction
* @alias module:@aeternity/aepp-sdk/es/contract/ga
Expand All @@ -116,32 +117,25 @@ async function createMetaTx (rawTransaction, authData, authFnName, options = {})
// Check if authData is callData or if it's an object prepare a callData from source and args
const { authCallData, gas } = await prepareGaParams(this)(authData, authFnName)
const opt = R.merge(this.Ae.defaults, options)
// Get transaction rlp binary
const rlpBinaryTx = decode(rawTransaction, 'tx')
// Wrap in SIGNED tx with empty signatures
const { rlpEncoded } = wrapInEmptySignedTx(rlpBinaryTx)
// Get abi
const { abiVersion } = await this.getVmVersion(TX_TYPE.contractCall)
// Prepare params for META tx
const wrappedTx = wrapInEmptySignedTx(unpackTx(rawTransaction))
const params = {
...opt,
tx: rlpEncoded,
tx: {
...wrappedTx,
tx: wrappedTx.txObject
},
gaId: await this.address(opt),
abiVersion: abiVersion,
authData: authCallData,
gas,
vsn: 2
}
// Calculate fee, get absolute ttl (ttl + height), get account nonce
const { fee } = await this.prepareTxParams(TX_TYPE.gaMeta, params)
// Build META tx
const { rlpEncoded: metaTxRlp } = buildTx(
{ ...params, fee: `${fee}` },
TX_TYPE.gaMeta,
{ vsn: 2 }
)
// Wrap in empty signed tx
const { tx } = wrapInEmptySignedTx(metaTxRlp)
// Send tx to the chain
return tx
return wrapInEmptySignedTx(metaTxRlp).tx
}
6 changes: 5 additions & 1 deletion src/tx/builder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ function buildFee (txType, { params, gas = 0, multiplier, vsn }) {
const { rlpEncoded: txWithOutFee } = buildTx({ ...params }, txType, { vsn })
const txSize = txWithOutFee.length
return TX_FEE_BASE_GAS(txType)
.plus(TX_FEE_OTHER_GAS(txType)({ txSize, relativeTtl: getOracleRelativeTtl(params, txType) }))
.plus(TX_FEE_OTHER_GAS(txType, txSize, {
relativeTtl: getOracleRelativeTtl(params, txType),
innerTxSize: [TX_TYPE.gaMeta, TX_TYPE.payingFor].includes(txType)
? params.tx.tx.encodedTx.rlpEncoded.length : 0
}))
.times(multiplier)
}

Expand Down
53 changes: 38 additions & 15 deletions src/tx/builder/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const OBJECT_TAG_ORACLES_TREE = 625
const OBJECT_TAG_ACCOUNTS_TREE = 626
const OBJECT_TAG_GA_ATTACH = 80
const OBJECT_TAG_GA_META = 81
const OBJECT_TAG_PAYING_FOR = 82
const OBJECT_TAG_SOPHIA_BYTE_CODE = 70

const TX_FIELD = (name, type, prefix) => [name, type, prefix]
Expand Down Expand Up @@ -225,6 +226,7 @@ export const TX_TYPE = {
// GA ACCOUNTS
gaAttach: 'gaAttach',
gaMeta: 'gaMeta',
payingFor: 'payingFor',
sophiaByteCode: 'sophiaByteCode'
}

Expand Down Expand Up @@ -324,6 +326,7 @@ export const OBJECT_ID_TX_TYPE = {
// GA Accounts
[OBJECT_TAG_GA_ATTACH]: TX_TYPE.gaAttach,
[OBJECT_TAG_GA_META]: TX_TYPE.gaMeta,
[OBJECT_TAG_PAYING_FOR]: TX_TYPE.payingFor,
[OBJECT_TAG_SOPHIA_BYTE_CODE]: TX_TYPE.sophiaByteCode
}

Expand Down Expand Up @@ -361,33 +364,39 @@ export const KEY_BLOCK_INTERVAL = 3

// MAP WITH FEE CALCULATION https://github.com/aeternity/protocol/blob/master/consensus/consensus.md#gas
export const TX_FEE_BASE_GAS = (txType) => {
switch (txType) {
// case TX_TYPE.gaMeta: // TODO investigate MetaTx calculation
case TX_TYPE.gaAttach:
case TX_TYPE.contractCreate:
return BigNumber(5 * BASE_GAS)
// Todo Implement meta tx fee calculation
case TX_TYPE.gaMeta:
case TX_TYPE.contractCall:
return BigNumber(12 * BASE_GAS)
default:
return BigNumber(BASE_GAS)
}
const factor = ({
[TX_TYPE.channelForceProgress]: 30,
[TX_TYPE.channelOffChain]: 0,
[TX_TYPE.channelOffChainCallContract]: 0,
[TX_TYPE.channelOffChainCreateContract]: 0,
[TX_TYPE.channelOffChainUpdateDeposit]: 0,
[TX_TYPE.channelOffChainUpdateWithdrawal]: 0,
[TX_TYPE.channelOffChainUpdateTransfer]: 0,
[TX_TYPE.contractCreate]: 5,
[TX_TYPE.contractCall]: 12,
[TX_TYPE.gaAttach]: 5,
[TX_TYPE.gaMeta]: 5,
[TX_TYPE.payingFor]: 1 / 5
})[txType] || 1
return new BigNumber(factor * BASE_GAS)
}

export const TX_FEE_OTHER_GAS = (txType) => ({ txSize, relativeTtl }) => {
export const TX_FEE_OTHER_GAS = (txType, txSize, { relativeTtl, innerTxSize }) => {
switch (txType) {
case TX_TYPE.oracleRegister:
case TX_TYPE.oracleExtend:
case TX_TYPE.oracleQuery:
case TX_TYPE.oracleResponse:
return BigNumber(txSize)
return new BigNumber(txSize)
.times(GAS_PER_BYTE)
.plus(
Math.ceil(32000 * relativeTtl / Math.floor(60 * 24 * 365 / KEY_BLOCK_INTERVAL))
)
case TX_TYPE.gaMeta:
case TX_TYPE.payingFor:
return new BigNumber(txSize).minus(innerTxSize).times(GAS_PER_BYTE)
default:
return BigNumber(txSize).times(GAS_PER_BYTE)
return new BigNumber(txSize).times(GAS_PER_BYTE)
}
}

Expand Down Expand Up @@ -547,6 +556,14 @@ const GA_META_TX_2 = [
TX_FIELD('tx', FIELD_TYPES.rlpBinary)
]

const PAYING_FOR_TX = [
...BASE_TX,
TX_FIELD('payerId', FIELD_TYPES.id, 'ak'),
TX_FIELD('nonce', FIELD_TYPES.int),
TX_FIELD('fee', FIELD_TYPES.int),
TX_FIELD('tx', FIELD_TYPES.rlpBinary)
]

const CONTRACT_CREATE_TX = [
...BASE_TX,
TX_FIELD('ownerId', FIELD_TYPES.id, 'ak'),
Expand Down Expand Up @@ -1018,6 +1035,9 @@ export const TX_SERIALIZATION_SCHEMA = {
},
[TX_TYPE.gaMeta]: {
2: TX_SCHEMA_FIELD(GA_META_TX_2, OBJECT_TAG_GA_META)
},
[TX_TYPE.payingFor]: {
1: TX_SCHEMA_FIELD(PAYING_FOR_TX, OBJECT_TAG_PAYING_FOR)
}
}

Expand Down Expand Up @@ -1154,6 +1174,9 @@ export const TX_DESERIALIZATION_SCHEMA = {
[OBJECT_TAG_GA_META]: {
2: TX_SCHEMA_FIELD(GA_META_TX_2, OBJECT_TAG_GA_META)
},
[OBJECT_TAG_PAYING_FOR]: {
1: TX_SCHEMA_FIELD(PAYING_FOR_TX, OBJECT_TAG_PAYING_FOR)
},
[OBJECT_TAG_CHANNEL_FORCE_PROGRESS_TX]: {
1: TX_SCHEMA_FIELD(CHANNEL_FORCE_PROGRESS_TX, OBJECT_TAG_CHANNEL_FORCE_PROGRESS_TX)
},
Expand Down
11 changes: 10 additions & 1 deletion src/tx/tx.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import * as R from 'ramda'
import ChainNode from '../chain/node'
import Tx from './'

import { buildTx, calculateFee } from './builder'
import { buildTx, calculateFee, unpackTx } from './builder'
import { ABI_VERSIONS, MIN_GAS_PRICE, PROTOCOL_VM_ABI, TX_TYPE, TX_TTL } from './builder/schema'
import { buildContractId } from './builder/helpers'
import { TxObject } from './tx-object'
Expand Down Expand Up @@ -411,6 +411,14 @@ async function gaAttachTx ({ ownerId, code, vmVersion, abiVersion, authFun, gas,
}
}

async function payingForTx ({ tx, payerId, ...args }) {
const params = { tx: unpackTx(tx), payerId }
const { fee, nonce } = await this.prepareTxParams(TX_TYPE.payingFor, {
...params, ...args, senderId: payerId
})
return buildTx({ ...params, ...args, fee, nonce }, TX_TYPE.payingFor).tx
}

/**
* Validated vm/abi version or get default based on transaction type and NODE version
*
Expand Down Expand Up @@ -535,6 +543,7 @@ const Transaction = ChainNode.compose(Tx, {
channelSettleTx,
channelSnapshotSoloTx,
gaAttachTx,
payingForTx,
getAccountNonce,
getVmVersion
}
Expand Down
21 changes: 14 additions & 7 deletions src/tx/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import { calculateFee, unpackTx } from './builder'
*/

const validators = [
async ({ encodedTx, signatures }, { account, node }) => {
async ({ encodedTx, signatures }, { account, node, parentTxTypes }) => {
if ((encodedTx ?? signatures) === undefined) return []
if (signatures.length !== 1) return [] // TODO: Support multisignature?
const networkId = Buffer.from(node.nodeNetworkId)
const txWithNetworkId = Buffer.concat([networkId, encodedTx.rlpEncoded])
const txHashWithNetworkId = Buffer.concat([networkId, hash(encodedTx.rlpEncoded)])
const prefix = Buffer.from([
node.nodeNetworkId,
...parentTxTypes.includes(TX_TYPE.payingFor) ? ['inner_tx'] : []
].join('-'))
const txWithNetworkId = Buffer.concat([prefix, encodedTx.rlpEncoded])
const txHashWithNetworkId = Buffer.concat([prefix, hash(encodedTx.rlpEncoded)])
const decodedPub = decode(account.id, 'ak')
if (verify(txWithNetworkId, signatures[0], decodedPub) ||
verify(txHashWithNetworkId, signatures[0], decodedPub)) return []
return [{
message: 'Signature cannot be verified, please ensure that you using the correct network and the correct private key for the sender address',
message: 'Signature cannot be verified, please ensure that you transaction have' +
' the correct prefix and the correct private key for the sender address',
key: 'InvalidSignature',
checkedKeys: ['encodedTx', 'signatures']
}]
Expand Down Expand Up @@ -58,9 +62,11 @@ const validators = [
checkedKeys: ['ttl']
}]
},
({ amount, fee, nameFee }, { account }) => {
({ amount, fee, nameFee, tx }, { account, parentTxTypes, txType }) => {
if ((amount ?? fee ?? nameFee) === undefined) return []
const cost = new BigNumber(fee).plus(nameFee || 0).plus(amount || 0)
.plus(txType === TX_TYPE.payingFor ? tx.tx.encodedTx.tx.fee : 0)
.minus(parentTxTypes.includes(TX_TYPE.payingFor) ? fee : 0)
if (cost.lte(account.balance)) return []
return [{
message: `Account balance ${account.balance} is not enough to execute the transaction that costs ${cost.toFixed()}`,
Expand Down Expand Up @@ -113,7 +119,8 @@ const validators = [
]

const getSenderAddress = tx => [
'senderId', 'accountId', 'ownerId', 'callerId', 'oracleId', 'fromId', 'initiator', 'gaId'
'senderId', 'accountId', 'ownerId', 'callerId',
'oracleId', 'fromId', 'initiator', 'gaId', 'payerId'
]
.map(key => tx[key])
.filter(a => a)
Expand Down
5 changes: 3 additions & 2 deletions test/integration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ export const ignoreVersion = process.env.IGNORE_VERSION || false
export const genesisAccount = MemoryAccount({ keypair: { publicKey, secretKey } })
export const account = Crypto.generateKeyPair()

export const BaseAe = async (params = {}) => Universal
export const BaseAe = async (params = {}, compose = {}) => Universal
.compose({
deepProps: { Ae: { defaults: { interval: 50, attempts: 1200 } } }
})({
})
.compose(compose)({
...params,
compilerUrl,
ignoreVersion,
Expand Down
Loading

0 comments on commit fbf204d

Please sign in to comment.