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 1 commit
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
Next Next commit
In progress work, serialize changes for new tx extensions and tests
  • Loading branch information
Bradley Hart committed Jun 10, 2021
commit f3c141bbdbef9b9defffa379bdda5f33e8da054c
12 changes: 4 additions & 8 deletions .github/eosjs-ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ RUN git clone https://github.com/EOSIO/eos \
&& cd eos \
&& git checkout develop

RUN git clone https://github.com/EOSIO/eosio.contracts \
&& cd eosio.contracts \
&& git checkout develop \
&& mkdir build && cd build && mkdir eosio.token && cd .. \
&& eosio-cpp -abigen ./contracts/eosio.token/src/eosio.token.cpp -o ./build/eosio.token/eosio.token.wasm -R ./contracts/eosio.token/ricardian/ -I ./contracts/eosio.token/include/

RUN git clone https://github.com/EOSIO/key-value-example-app.git \
&& cd key-value-example-app \
&& eosio-cpp -abigen ./contracts/kv_todo/src/kv_todo.cpp -o ./contracts/kv_todo/build/kv_todo.wasm -R ./contracts/kv_todo/ricardian/ -I ./contracts/kv_todo/include/
Expand All @@ -36,12 +30,14 @@ RUN cd cfhello \


FROM eosio/eosio:${EOSBRANCH}
ENTRYPOINT ["nodeos", "--data-dir", "/root/.local/share", "-e", "-p", "eosio", "--replay-blockchain", "--plugin", "eosio::producer_plugin", "--plugin", "eosio::chain_api_plugin", "--plugin", "eosio::trace_api_plugin", "--trace-no-abis", "--plugin", "eosio::db_size_api_plugin", "--plugin", "eosio::http_plugin", "--http-server-address=0.0.0.0:8888", "--access-control-allow-origin=*", "--contracts-console", "--http-validate-host=false", "--enable-account-queries=true", "--verbose-http-errors", "--max-transaction-time=100"]
ENTRYPOINT ["nodeos", "--data-dir", "/root/.local/share", "-e", "-p", "eosio", "--replay-blockchain", "--plugin", "eosio::producer_plugin", "--plugin", "eosio::producer_api_plugin", "--plugin", "eosio::chain_api_plugin", "--plugin", "eosio::trace_api_plugin", "--trace-no-abis", "--plugin", "eosio::db_size_api_plugin", "--plugin", "eosio::http_plugin", "--http-server-address=0.0.0.0:8888", "--access-control-allow-origin=*", "--contracts-console", "--http-validate-host=false", "--enable-account-queries=true", "--verbose-http-errors", "--max-transaction-time=100"]
WORKDIR /root
RUN mkdir -p "/opt/eosio/bin/contracts"
COPY --from=contracts /root/eos/contracts/contracts/eosio.bios/bin/* /opt/eosio/bin/contracts/eosio.bios/
COPY --from=contracts /root/eos/contracts/contracts/eosio.boot/bin/* /opt/eosio/bin/contracts/eosio.boot/
COPY --from=contracts /root/eosio.contracts/build/ /opt/eosio/bin/contracts
COPY --from=contracts /root/eos/unittests/contracts/eosio.system/* /opt/eosio/bin/contracts/eosio.system/
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/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
13 changes: 13 additions & 0 deletions .github/eosjs-ci/scripts/deploy_contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ activate_feature "4a90c00d55454dc5b059055ca213579c6ea856967712a56017487886a4d4cc
activate_feature "1a99a59d87e06e09ec5b028a9cbb7749b4a5ad8819004365d02dc4379a8b7241"
activate_feature "bf61537fd21c61a60e542a5d66c3f6a78da0589336868307f94a82bccea84e88"
activate_feature "5443fcf88330c586bc0e5f3dee10e7f63c76c00249c87fe4fbf7f38c082006b4"
activate_feature "808c49387292c34ccb3970e00b08a690b6b3370c1cbcec46d46c19d5dfafab03"

sleep 1s
setabi eosio $CONTRACTS_DIR/eosio.bios/eosio.bios.abi
Expand All @@ -214,6 +215,14 @@ sleep 1s
setabi returnvalue $CONTRACTS_DIR/action_return_value/action_return_value.abi
setcode returnvalue $CONTRACTS_DIR/action_return_value/action_return_value.wasm

sleep 1s
setabi eosio.msig $CONTRACTS_DIR/eosio.msig/eosio.msig.abi
setcode eosio.msig $CONTRACTS_DIR/eosio.msig/eosio.msig.wasm

sleep 1s
setabi eosio $CONTRACTS_DIR/eosio.system/eosio.system.abi
setcode eosio $CONTRACTS_DIR/eosio.system/eosio.system.wasm

sleep 1s
setabi eosio.token $CONTRACTS_DIR/eosio.token/eosio.token.abi
setcode eosio.token $CONTRACTS_DIR/eosio.token/eosio.token.wasm
Expand All @@ -229,6 +238,10 @@ cleos push action todo upsert '["bf581bee-9f2c-447b-94ad-78e4984b6f51", "todo",
cleos push action todo upsert '["b7b0d09d-a82b-44d9-b067-3bae2d02917e", "todo", "Start Blockchain", false]' -p todo@active
cleos push action todo upsert '["ac8acfe7-cd4e-4d22-8400-218b697a4517", "todo", "Deploy Hello World Contract", false]' -p todo@active

cleos push action eosio init '["0", "4,SYS"]' -p eosio@active

cleos system newaccount eosio --transfer payer $EXAMPLE_ACCOUNT_PUBLIC_KEY --stake-net "100000000.0000 SYS" --stake-cpu "100000000.0000 SYS" --buy-ram-kbytes 8192

echo "All done initializing the blockchain"

if [[ -z $NODEOS_RUNNING ]]; then
Expand Down
1 change: 1 addition & 0 deletions src/eosjs-api-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export interface TransactionTrace {
account_ram_delta: AccountDelta|null;
except: string|null;
error_code: number|null;
bill_to_accounts: string[];
}

export interface TransactResult {
Expand Down
4 changes: 1 addition & 3 deletions src/eosjs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,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 @@ -93,7 +91,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
3 changes: 2 additions & 1 deletion src/eosjs-rpc-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,8 @@ export interface TraceApiTransaction {
cpu_usage_us?: number;
net_usage_words?: number;
signatures?: string[];
transaction_header?: TraceApiTransactionHeader
transaction_header?: TraceApiTransactionHeader,
bill_to_accounts: string[]
}

/** Return value of `/v1/trace_api/get_block` */
Expand Down
117 changes: 117 additions & 0 deletions src/eosjs-serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,44 @@ function deserializeObject(this: Type, buffer: SerialBuffer, state?: SerializerS
return result;
}

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

function serializeTransactionExtension(
this: Type, buffer: SerialBuffer, data: any, state?: SerializerState, allowExtensions?: boolean
): void {
const transactionExtension = transactionExtensions.find(extension => {
return extension.keys.sort().every((element, index) => element === Object.keys(data).sort()[index]);
});
if (transactionExtension === undefined) {
throw new Error(`Transaction Extension could not be determined: ${data}`);
}
const extensionBuffer = new SerialBuffer({ textEncoder: buffer.textEncoder, textDecoder: buffer.textDecoder});
const types = getTypesFromAbi(createTransactionExtensionTypes());
types.get(transactionExtension.type).serialize(extensionBuffer, data, state, false);

buffer.pushVaruint32([transactionExtension.id, arrayToHex(extensionBuffer.asUint8Array())].length);
this.fields[0].type.serialize(buffer, transactionExtension.id, state, false);
this.fields[1].type.serialize(buffer, arrayToHex(extensionBuffer.asUint8Array()), state, false);
}

function deserializeTransactionExtension(this: Type, buffer: SerialBuffer, state?: SerializerState, allowExtensions?: boolean): any {
const result = [] as any;
const len = buffer.getVaruint32();
result.push(this.fields[0].type.deserialize(buffer, state, false));
result.push(this.fields[1].type.deserialize(buffer, state, false));
const transactionExtension = transactionExtensions.find(extension => extension.id === result[0]);
if (transactionExtension === undefined) {
throw new Error(`Transaction Extension could not be determined: ${result}`);
}
const types = getTypesFromAbi(createTransactionExtensionTypes());

const extensionBuffer = new SerialBuffer({ textEncoder: buffer.textEncoder, textDecoder: buffer.textDecoder });
extensionBuffer.pushArray(hexToUint8Array(result[1]));
return types.get(transactionExtension.type).deserialize(extensionBuffer, state, false);
}

interface CreateTypeArgs {
name?: string;
aliasOfName?: string;
Expand Down Expand Up @@ -1244,6 +1282,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: serializeTransactionExtension,
deserialize: deserializeTransactionExtension,
}));
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
31 changes: 31 additions & 0 deletions src/tests/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,36 @@ const transactWithReturnValue = async () => {
});
};

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

const broadcastResult = async (signaturesAndPackedTransaction) => await api.pushSignedTransaction(signaturesAndPackedTransaction);

const transactShouldFail = async () => await api.transact({
Expand Down Expand Up @@ -210,5 +240,6 @@ module.exports = {
transactWithShorthandTxJsonContextFreeAction,
transactWithShorthandTxJsonContextFreeData,
transactWithReturnValue,
transactWithResourcePayer,
rpcShouldFail
};
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('throws appropriate error message without configuration object or TAPOS in place', async () => {
try {
failedAsPlanned = true;
Expand Down
6 changes: 5 additions & 1 deletion src/tests/type-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ describe('Chain API Plugin Endpoints', () => {
},
'except&': 'string',
'error_code&': 'number',
bill_to_accounts: 'string',
},
};
verifyType(result, transactResult);
Expand Down Expand Up @@ -999,6 +1000,7 @@ describe('Chain API Plugin Endpoints', () => {
},
'except&': 'string',
'error_code&': 'number',
bill_to_accounts: 'string',
},
};
result.forEach((transaction: TransactResult) => {
Expand Down Expand Up @@ -1096,6 +1098,7 @@ describe('Chain API Plugin Endpoints', () => {
},
'except&': 'string',
'error_code&': 'number',
bill_to_accounts: 'string',
},
};
verifyType(result, transactResult);
Expand Down Expand Up @@ -1150,7 +1153,8 @@ describe('Trace API Plugin Endpoints', () => {
cpu_usage_us: 'number',
net_usage_words: 'number',
signatures: 'string',
transaction_header: 'any'
transaction_header: 'any',
bill_to_accounts: 'string',
},
};
verifyType(result, traceApiGetBlockResult);
Expand Down
54 changes: 54 additions & 0 deletions src/tests/web.html
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,60 @@
resultsLabel.innerText = FAILED;
return false;
}

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

}

const testWithResourcePayerTx = async (e) => {
resultsLabel = e.target;
resultsLabel.innerText = EXECUTING;

try {
transactionResponse = await resourcePayerTx();
} catch (error) {
resultsLabel.className = 'failed';
resultsLabel.innerText = FAILED;
console.error('Transact Resource Payer Test Failure: ', error.message);
return false;
}

if (transactionResponse.transaction_id) {
resultsLabel.className = "success";
resultsLabel.innerText = SUCCESS;
return true;
}
resultsLabel.className = 'failed';
resultsLabel.innerText = FAILED;
return false;
}

const transactShouldFail = async () => await api.transact({
actions: [{
Expand Down