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

Commit

Permalink
Merge pull request #964 from EOSIO/transaction_sponsorship
Browse files Browse the repository at this point in the history
Support for transaction sponsorship/resource payer
  • Loading branch information
Brad Hart authored Jul 1, 2021
2 parents ee54e3b + 713a422 commit 9dafdfa
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 129 deletions.
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

0 comments on commit 9dafdfa

Please sign in to comment.