Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Support for transaction sponsorship/resource payer #964

Merged
merged 16 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/eosjs-ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ COPY --from=contracts /root/eos/unittests/contracts/eosio.system/* /opt/eosio/bi
COPY --from=contracts /root/eos/unittests/contracts/eosio.msig/* /opt/eosio/bin/contracts/eosio.msig/
COPY --from=contracts /root/eos/unittests/contracts/eosio.token/* /opt/eosio/bin/contracts/eosio.token/
COPY --from=contracts /root/eosio.cdt/build/read_only_query_tests/* /opt/eosio/bin/contracts/read_only_query_tests/
COPY --from=contracts /root/eosio.contracts/build/ /opt/eosio/bin/contracts
COPY --from=contracts /root/key-value-example-app/contracts/kv_todo/build/* /opt/eosio/bin/contracts/kv_todo/
COPY --from=contracts /root/return-values-example-app/contracts/action_return_value/build/* /opt/eosio/bin/contracts/action_return_value/
COPY --from=contracts /root/cfhello/build/* /opt/eosio/bin/contracts/cfhello/
Expand Down
31 changes: 31 additions & 0 deletions docs/how-to-guides/20_how-to-set-a-payer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
After the release of v2.2 of nodeos, the transaction sponsorship feature is available to sponsor the resources for a transaction. To set a separate payer for the resources for a transaction, add a `resource_payer` object to your transaction that specifies the `payer`, `max_net_bytes`, `max_cpu_us`, and `max_memory_bytes`. This functionality requires the `RESOURCE_PAYER` feature to be enabled on the chain.

A typical use-case for this feature has a service or application pay for the resources of a transaction instead of their users. Since authorization is required for both the user in the transaction and the payer, a possible workflow would have the transaction signed by the user's wallet application and then also signed by the service/application before sent to nodeos.

```javascript
{
resource_payer: {
payer: 'alice',
max_net_bytes: 4096,
max_cpu_us: 400,
max_memory_bytes: 0
},
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'bob',
permission: 'active',
}, {
actor: 'alice',
permission: 'active',
}],
data: {
from: 'bob',
to: 'alice',
quantity: '0.0001 SYS',
memo: 'resource payer',
},
}]
}
```
8 changes: 8 additions & 0 deletions src/eosjs-api-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ export interface SignatureProvider {
sign: (args: SignatureProviderArgs) => Promise<PushTransactionArgs>;
}

export interface ResourcePayer {
payer: string;
max_net_bytes: number|string;
max_cpu_us: number|string;
max_memory_bytes: number|string;
}

export interface Transaction {
expiration?: string;
ref_block_num?: number;
Expand All @@ -83,6 +90,7 @@ export interface Transaction {
context_free_data?: Uint8Array[];
actions: Action[];
transaction_extensions?: [number, string][];
resource_payer?: ResourcePayer;
}

/** Optional transact configuration object */
Expand Down
47 changes: 44 additions & 3 deletions src/eosjs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ import {
} from './eosjs-rpc-interfaces';
import * as ser from './eosjs-serialize';

const transactionAbi = require('../src/transaction.abi.json');

export class Api {
/** Issues RPC calls */
public rpc: JsonRpc;
Expand Down Expand Up @@ -94,7 +92,7 @@ export class Api {
this.textDecoder = args.textDecoder;

this.abiTypes = ser.getTypesFromAbi(ser.createAbiTypes());
this.transactionTypes = ser.getTypesFromAbi(ser.createInitialTypes(), transactionAbi);
this.transactionTypes = ser.getTypesFromAbi(ser.createTransactionTypes());
}

/** Decodes an abi as Uint8Array into json. */
Expand Down Expand Up @@ -223,6 +221,47 @@ export class Api {
return this.deserialize(buffer, 'transaction');
}

private transactionExtensions = [
{ id: 1, type: 'resource_payer', keys: ['payer', 'max_net_bytes', 'max_cpu_us', 'max_memory_bytes'] },
];

// Order of adding to transaction_extension is transaction_extension id ascending
public serializeTransactionExtensions(transaction: Transaction): [number, string][] {
let transaction_extensions: [number, string][] = [];
if (transaction.resource_payer) {
const extensionBuffer = new ser.SerialBuffer({ textEncoder: this.textEncoder, textDecoder: this.textDecoder });
const types = ser.getTypesFromAbi(ser.createTransactionExtensionTypes());
types.get('resource_payer').serialize(extensionBuffer, transaction.resource_payer);
transaction_extensions = [...transaction_extensions, [1, ser.arrayToHex(extensionBuffer.asUint8Array())]];
}
return transaction_extensions;
};

// Usage: transaction = {...transaction, ...this.deserializeTransactionExtensions(transaction.transaction_extensions)}
public deserializeTransactionExtensions(data: [number, string][]): any[] {
const transaction = {} as any;
data.forEach((extensionData: [number, string]) => {
const transactionExtension = this.transactionExtensions.find(extension => extension.id === extensionData[0]);
if (transactionExtension === undefined) {
throw new Error(`Transaction Extension could not be determined: ${extensionData}`);
}
const types = ser.getTypesFromAbi(ser.createTransactionExtensionTypes());
const extensionBuffer = new ser.SerialBuffer({ textEncoder: this.textEncoder, textDecoder: this.textDecoder });
extensionBuffer.pushArray(ser.hexToUint8Array(extensionData[1]));
const deserializedObj = types.get(transactionExtension.type).deserialize(extensionBuffer);
if (extensionData[0] === 1) {
transaction.resource_payer = deserializedObj;
}
});
return transaction;
};

// Transaction extensions are serialized and moved to `transaction_extensions`, deserialized objects are not needed on the transaction
public deleteTransactionExtensionObjects(transaction: Transaction): Transaction {
delete transaction.resource_payer;
return transaction;
}

/** Convert actions to hex */
public async serializeActions(actions: ser.Action[]): Promise<ser.SerializedAction[]> {
return await Promise.all(actions.map(async (action) => {
Expand Down Expand Up @@ -325,9 +364,11 @@ export class Api {
const abis: BinaryAbi[] = await this.getTransactionAbis(transaction);
transaction = {
...transaction,
transaction_extensions: await this.serializeTransactionExtensions(transaction),
context_free_actions: await this.serializeActions(transaction.context_free_actions || []),
actions: await this.serializeActions(transaction.actions)
};
transaction = this.deleteTransactionExtensionObjects(transaction);
const serializedTransaction = this.serializeTransaction(transaction);
const serializedContextFreeData = this.serializeContextFreeData(transaction.context_free_data);
let pushTransactionArgs: PushTransactionArgs = {
Expand Down
99 changes: 99 additions & 0 deletions src/eosjs-serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,26 @@ function deserializeObject(this: Type, buffer: SerialBuffer, state?: SerializerS
return result;
}

function serializePair(
this: Type, buffer: SerialBuffer, data: any, state?: SerializerState, allowExtensions?: boolean
): void {
buffer.pushVaruint32(data.length);
data.forEach((item: [number, string]) => {
this.fields[0].type.serialize(buffer, item[0], state, allowExtensions);
this.fields[1].type.serialize(buffer, item[1], state, allowExtensions);
});
}

function deserializePair(this: Type, buffer: SerialBuffer, state?: SerializerState, allowExtensions?: boolean): any {
const result = [] as any;
const len = buffer.getVaruint32();
for (let i = 0; i < len; ++i) {
result.push(this.fields[0].type.deserialize(buffer, state, allowExtensions));
result.push(this.fields[1].type.deserialize(buffer, state, allowExtensions));
}
return result;
}

interface CreateTypeArgs {
name?: string;
aliasOfName?: string;
Expand Down Expand Up @@ -1244,6 +1264,85 @@ export const createAbiTypes = (): Map<string, Type> => {
return initialTypes;
};

export const createTransactionExtensionTypes = (): Map<string, Type> => {
const initialTypes = createInitialTypes();
initialTypes.set('resource_payer', createType({
name: 'resource_payer',
baseName: '',
fields: [
{ name: 'payer', typeName: 'name', type: null },
{ name: 'max_net_bytes', typeName: 'uint64', type: null },
{ name: 'max_cpu_us', typeName: 'uint64', type: null },
{ name: 'max_memory_bytes', typeName: 'uint64', type: null },
],
serialize: serializeStruct,
deserialize: deserializeStruct,
}));
return initialTypes;
};

export const createTransactionTypes = (): Map<string, Type> => {
const initialTypes = createInitialTypes();
initialTypes.set('permission_level', createType({
name: 'permission_level',
baseName: '',
fields: [
{ name: 'actor', typeName: 'name', type: null },
{ name: 'permission', typeName: 'name', type: null },
],
serialize: serializeStruct,
deserialize: deserializeStruct,
}));
initialTypes.set('action', createType({
name: 'action',
baseName: '',
fields: [
{ name: 'account', typeName: 'name', type: null },
{ name: 'name', typeName: 'name', type: null },
{ name: 'authorization', typeName: 'permission_level[]', type: null },
{ name: 'data', typeName: 'bytes', type: null },
],
serialize: serializeStruct,
deserialize: deserializeStruct,
}));
initialTypes.set('extension', createType({
name: 'extension',
baseName: '',
fields: [
{ name: 'type', typeName: 'uint16', type: null },
{ name: 'data', typeName: 'bytes', type: null },
],
serialize: serializePair,
deserialize: deserializePair,
}));
initialTypes.set('transaction_header', createType({
name: 'transaction_header',
baseName: '',
fields: [
{ name: 'expiration', typeName: 'time_point_sec', type: null },
{ name: 'ref_block_num', typeName: 'uint16', type: null },
{ name: 'ref_block_prefix', typeName: 'uint32', type: null },
{ name: 'max_net_usage_words', typeName: 'varuint32', type: null },
{ name: 'max_cpu_usage_ms', typeName: 'uint8', type: null },
{ name: 'delay_sec', typeName: 'varuint32', type: null },
],
serialize: serializeStruct,
deserialize: deserializeStruct,
}));
initialTypes.set('transaction', createType({
name: 'transaction',
baseName: 'transaction_header',
fields: [
{ name: 'context_free_actions', typeName: 'action[]', type: null },
{ name: 'actions', typeName: 'action[]', type: null },
{ name: 'transaction_extensions', typeName: 'extension', type: null }
],
serialize: serializeStruct,
deserialize: deserializeStruct,
}));
return initialTypes;
};

/** Get type from `types` */
export const getType = (types: Map<string, Type>, name: string): Type => {
const type = types.get(name);
Expand Down
41 changes: 41 additions & 0 deletions src/tests/eosjs-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,46 @@ describe('eosjs-api', () => {

expect(firstSerializedAction).toEqual(secondSerializedAction);
});

it('confirms the transaction extension serialization is reciprocal', async () => {
const resourcePayerTrx = {
expiration: '2021-06-28T15:55:37.000',
ref_block_num: 2097,
ref_block_prefix: 1309445478,
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'bob',
permission: 'active',
}, {
actor: 'alice',
permission: 'active',
}],
data: '0000000000000E3D0000000000855C34010000000000000004535953000000000E7265736F75726365207061796572'
}],
context_free_actions: [] as any,
resource_payer: {
payer: 'payer',
max_net_bytes: '4096',
max_cpu_us: '250',
max_memory_bytes: '0'
}
};
const serialized = [[1, '0000000080ABBCA90010000000000000FA000000000000000000000000000000']];
const deserialized = {
resource_payer: {
payer: 'payer',
max_net_bytes: '4096',
max_cpu_us: '250',
max_memory_bytes: '0'
}
};

const serializedTransactionExtensions = api.serializeTransactionExtensions(resourcePayerTrx);
expect(serializedTransactionExtensions).toEqual(serialized);
const deserializedTransactionExtensions = api.deserializeTransactionExtensions(serialized);
expect(deserializedTransactionExtensions).toEqual(deserialized);
});
});
});
32 changes: 32 additions & 0 deletions src/tests/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,37 @@ const transactWithReturnValue = async () => {
});
};

const transactWithResourcePayer = async () => {
return await api.transact({
resource_payer: {
payer: 'alice',
max_net_bytes: 4096,
max_cpu_us: 400,
max_memory_bytes: 0
},
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'bob',
permission: 'active',
}, {
actor: 'alice',
permission: 'active',
}],
data: {
from: 'bob',
to: 'alice',
quantity: '0.0001 SYS',
memo: 'resource payer',
},
}]
}, {
blocksBehind: 3,
expireSeconds: 30
});
};

const readOnlyQuery = async () => {
return await api.transact({
actions: [{
Expand Down Expand Up @@ -253,6 +284,7 @@ module.exports = {
transactWithShorthandTxJsonContextFreeAction,
transactWithShorthandTxJsonContextFreeData,
transactWithReturnValue,
transactWithResourcePayer,
readOnlyQuery,
readOnlyFailureTrace,
rpcShouldFail
Expand Down
5 changes: 5 additions & 0 deletions src/tests/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ describe('Node JS environment', () => {
expect(transactionResponse.processed.action_traces[0].return_value_data).toEqual(expectedValue);
});

it('transacts with resource payer', async () => {
transactionResponse = await tests.transactWithResourcePayer();
expect(Object.keys(transactionResponse)).toContain('transaction_id');
});

it('confirms the return value of the read-only query', async () => {
const expectedValue = [
{'age': 25, 'gender': 1, 'id': 1, 'name': 'Bob Smith'},
Expand Down
10 changes: 2 additions & 8 deletions src/tests/type-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,10 +426,7 @@ describe('Chain API Plugin Endpoints', () => {
'data?': 'any',
'hex_data?': 'string',
},
'transaction_extensions?': {
type: 'number',
data: 'string',
},
'transaction_extensions?': '[number, string]',
},
},
},
Expand Down Expand Up @@ -698,10 +695,7 @@ describe('Chain API Plugin Endpoints', () => {
'data?': 'any',
'hex_data?': 'string',
},
'transaction_extensions?': {
type: 'number',
data: 'string',
},
'transaction_extensions?': '[number, string]',
'deferred_transaction_generation?': {
sender_trx_id: 'string',
sender_id: 'string',
Expand Down
Loading