Skip to content

Commit

Permalink
feat(tx-builder): support recursive types
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jan 25, 2023
1 parent 2d23be6 commit 4f4dff6
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 43 deletions.
6 changes: 3 additions & 3 deletions src/channel/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,14 @@ export default class Channel {
if (this._state == null) {
return null;
}
const { tag, round } = unpackTx(this._state, Tag.SignedTx).encodedTx;
switch (tag) {
const params = unpackTx(this._state, Tag.SignedTx).encodedTx;
switch (params.tag) {
case Tag.ChannelCreateTx:
return 1;
case Tag.ChannelOffChainTx:
case Tag.ChannelWithdrawTx:
case Tag.ChannelDepositTx:
return round;
return params.round;
default:
return null;
}
Expand Down
9 changes: 6 additions & 3 deletions src/channel/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ContractCallReturnType } from '../apis/node';
import { ContractCallObject } from '../contract/Contract';
import Channel from './Base';
import ChannelSpend from './Spend';
import { UnexpectedChannelMessageError } from '../utils/errors';
import { ChannelError, UnexpectedChannelMessageError } from '../utils/errors';
import { unpackTx } from '../tx/builder';
import { encodeContractAddress } from '../utils/crypto';

Expand Down Expand Up @@ -152,14 +152,17 @@ export default class ChannelContract extends ChannelSpend {
state2: ChannelState,
): ChannelFsm => (
awaitingCompletion(this, message2, state2, () => {
const { round } = unpackTx(message2.params.data.state, Tag.SignedTx).encodedTx;
const params = unpackTx(message2.params.data.state, Tag.SignedTx).encodedTx;
if (params.tag !== Tag.ChannelOffChainTx) {
throw new ChannelError(`Tag should be ${Tag[Tag.ChannelOffChainTx]}, got ${Tag[params.tag]} instead`);
}
const addressKey = this._options.role === 'initiator'
? 'initiatorId' : 'responderId';
const owner = this._options[addressKey];
changeState(this, message2.params.data.state);
state2.resolve({
accepted: true,
address: encodeContractAddress(owner, round),
address: encodeContractAddress(owner, params.round),
signedTx: message2.params.data.state,
});
return { handler: channelOpen };
Expand Down
34 changes: 25 additions & 9 deletions src/tx/builder/SchemaTypes.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Field } from './field-types';
import { Field as OriginalField } from './field-types';
import { UnionToIntersection } from '../../utils/other';

// TODO: figure out why this override is necessary
export interface Field extends OriginalField {
serialize: (...args: any[]) => any;
}

type NullablePartial<
T,
NK extends keyof T = { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T],
> = Partial<Pick<T, NK>> & Omit<T, NK>;

type Or<A, B> = A extends undefined ? B : A;

type TxParamsBySchemaInternal<SchemaItem> = NullablePartial<{
type TxParamsBySchemaInternal<SchemaItem> = {
-readonly [key in keyof SchemaItem]: SchemaItem[key] extends Field
? Parameters<SchemaItem[key]['serialize']>[0]
: never;
}>;
};

type TxParamsBySchemaInternalParams<
SchemaItem,
Expand All @@ -22,16 +27,26 @@ SchemaItemValues extends Field
? Or<Parameters<SchemaItemValues['serialize']>[2], {}> : never
>;

type PickIsRec<SchemaItem, Recursive extends boolean> = {
[Key in keyof SchemaItem
as SchemaItem[Key] extends Field & { recursiveType: true }
? (Recursive extends true ? Key : never)
: (Recursive extends true ? never : Key)]: SchemaItem[Key];
};

type TxParamsBySchema<SchemaItem> = SchemaItem extends Object
? TxParamsBySchemaInternal<SchemaItem> & TxParamsBySchemaInternalParams<SchemaItem> : never;
? TxParamsBySchemaInternal<PickIsRec<SchemaItem, true>> &
NullablePartial<TxParamsBySchemaInternal<PickIsRec<SchemaItem, false>>> &
TxParamsBySchemaInternalParams<PickIsRec<SchemaItem, false>>
: never;

type TxParamsAsyncBySchemaInternal<SchemaItem> = NullablePartial<{
type TxParamsAsyncBySchemaInternal<SchemaItem> = {
-readonly [key in keyof SchemaItem]: SchemaItem[key] extends Field & { prepare: Function }
? Parameters<SchemaItem[key]['prepare']>[0]
: SchemaItem[key] extends Field
? Parameters<SchemaItem[key]['serialize']>[0]
: never;
}>;
};

type TxParamsAsyncBySchemaInternalParams<
SchemaItem,
Expand All @@ -42,9 +57,10 @@ SchemaItemValues extends Field & { prepare: Function }
>;

type TxParamsAsyncBySchema<SchemaItem> = SchemaItem extends Object
? TxParamsAsyncBySchemaInternal<SchemaItem>
& TxParamsAsyncBySchemaInternalParams<SchemaItem>
& TxParamsBySchemaInternalParams<SchemaItem>
? TxParamsAsyncBySchemaInternal<PickIsRec<SchemaItem, true>>
& NullablePartial<TxParamsAsyncBySchemaInternal<PickIsRec<SchemaItem, false>>>
& TxParamsAsyncBySchemaInternalParams<PickIsRec<SchemaItem, false>>
& TxParamsBySchemaInternalParams<PickIsRec<SchemaItem, false>>
: never;

type TxUnpackedBySchema<SchemaItem> = {
Expand Down
19 changes: 14 additions & 5 deletions src/tx/builder/field-types/fee.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js';
import { IllegalArgumentError } from '../../../utils/errors';
import { ArgumentError, IllegalArgumentError } from '../../../utils/errors';
import { MIN_GAS_PRICE, Tag } from '../constants';
import coinAmount from './coin-amount';
import { isKeyOfObject } from '../../../utils/other';
Expand Down Expand Up @@ -101,12 +101,21 @@ export function buildFee(
const { length } = decode(builtTx);
const txObject = unpackTx(builtTx);

let innerTxSize = 0;
if (txObject.tag === Tag.GaMetaTx || txObject.tag === Tag.PayingForTx) {
if (txObject.tx.tag !== Tag.SignedTx) {
throw new ArgumentError(
`Payload of ${Tag[txObject.tag]}`,
Tag[Tag.SignedTx],
Tag[txObject.tx.tag],
);
}
innerTxSize = decode(buildTx(txObject.tx.encodedTx)).length;
}

return TX_FEE_BASE_GAS(txObject.tag)
.plus(TX_FEE_OTHER_GAS(txObject.tag, length, {
relativeTtl: getOracleRelativeTtl(txObject),
innerTxSize: txObject.tag === Tag.GaMetaTx || txObject.tag === Tag.PayingForTx
? decode(buildTx(txObject.tx.encodedTx)).length
: 0,
relativeTtl: getOracleRelativeTtl(txObject), innerTxSize,
}))
.times(MIN_GAS_PRICE);
}
Expand Down
1 change: 1 addition & 0 deletions src/tx/builder/field-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface Field {
serialize: (value: any, options: any, parameters: any) => BinaryData;
prepare?: (value: any, options: any, parameters: any) => Promise<any>;
deserialize: (value: BinaryData, options: any) => any;
recursiveType?: boolean;
}

export {
Expand Down
60 changes: 41 additions & 19 deletions src/tx/builder/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
address, pointers, entry, enumeration, mptree, shortUIntConst, string, encoded, raw,
array, boolean, ctVersion, abiVersion, ttl, nonce,
} from './field-types';
import { Encoding } from '../../utils/encoder';
import { Encoded, Encoding } from '../../utils/encoder';
import { idTagToEncoding } from './field-types/address';

export enum ORACLE_TTL_TYPES {
Expand All @@ -36,6 +36,28 @@ export enum CallReturnType {
Revert = 2,
}

// TODO: figure out how to omit overriding types of recursive fields
interface EntryAny {
serialize: (value: TxParams | Uint8Array | Encoded.Transaction) => Buffer;
deserialize: (value: Buffer) => TxUnpacked;
recursiveType: true;
}

interface EntryAnyArray {
serialize: (value: Array<TxParams | Uint8Array | Encoded.Transaction>) => Buffer[];
deserialize: (value: Buffer[]) => TxUnpacked[];
recursiveType: true;
}

interface EntryTreesPoi {
serialize: (value: TxParams & { tag: Tag.TreesPoi } | Uint8Array | Encoded.Transaction) => Buffer;
deserialize: (value: Buffer) => TxUnpacked & { tag: Tag.TreesPoi };
recursiveType: true;
}

const entryAny = entry() as unknown as EntryAny;
const entryTreesPoi = entry(Tag.TreesPoi) as unknown as EntryTreesPoi;

/**
* @see {@link https://github.com/aeternity/protocol/blob/c007deeac4a01e401238412801ac7084ac72d60e/serializations.md#accounts-version-1-basic-accounts}
*/
Expand All @@ -56,7 +78,7 @@ export const txSchema = [{
tag: shortUIntConst(Tag.SignedTx),
version: shortUIntConst(1, true),
signatures: array(raw),
encodedTx: entry(),
encodedTx: entryAny,
}, {
tag: shortUIntConst(Tag.SpendTx),
version: shortUIntConst(1, true),
Expand Down Expand Up @@ -259,7 +281,7 @@ export const txSchema = [{
channelId: address(Encoding.Channel),
fromId: address(Encoding.AccountAddress),
payload: encoded(Encoding.Transaction),
poi: entry(Tag.TreesPoi),
poi: entryTreesPoi,
ttl,
fee,
nonce: nonce('fromId'),
Expand All @@ -269,7 +291,7 @@ export const txSchema = [{
channelId: address(Encoding.Channel),
fromId: address(Encoding.AccountAddress),
payload: encoded(Encoding.Transaction),
poi: entry(Tag.TreesPoi),
poi: entryTreesPoi,
ttl,
fee,
nonce: nonce('fromId'),
Expand Down Expand Up @@ -405,16 +427,16 @@ export const txSchema = [{
}, {
tag: shortUIntConst(Tag.StateTrees),
version: shortUIntConst(1, true),
contracts: entry(),
calls: entry(),
channels: entry(),
ns: entry(),
oracles: entry(),
accounts: entry(),
contracts: entryAny,
calls: entryAny,
channels: entryAny,
ns: entryAny,
oracles: entryAny,
accounts: entryAny,
}, {
tag: shortUIntConst(Tag.Mtree),
version: shortUIntConst(1, true),
values: array(entry()),
values: array(entry()) as unknown as EntryAnyArray,
}, {
tag: shortUIntConst(Tag.MtreeValue),
version: shortUIntConst(1, true),
Expand All @@ -423,27 +445,27 @@ export const txSchema = [{
}, {
tag: shortUIntConst(Tag.ContractsMtree),
version: shortUIntConst(1, true),
contracts: entry(),
contracts: entryAny,
}, {
tag: shortUIntConst(Tag.CallsMtree),
version: shortUIntConst(1, true),
calls: entry(),
calls: entryAny,
}, {
tag: shortUIntConst(Tag.ChannelsMtree),
version: shortUIntConst(1, true),
channels: entry(),
channels: entryAny,
}, {
tag: shortUIntConst(Tag.NameserviceMtree),
version: shortUIntConst(1, true),
mtree: entry(),
mtree: entryAny,
}, {
tag: shortUIntConst(Tag.OraclesMtree),
version: shortUIntConst(1, true),
otree: entry(),
otree: entryAny,
}, {
tag: shortUIntConst(Tag.AccountsMtree),
version: shortUIntConst(1, true),
accounts: entry(),
accounts: entryAny,
}, {
tag: shortUIntConst(Tag.GaAttachTx),
version: shortUIntConst(1, true),
Expand All @@ -466,14 +488,14 @@ export const txSchema = [{
fee,
gasLimit,
gasPrice,
tx: entry(),
tx: entryAny,
}, {
tag: shortUIntConst(Tag.PayingForTx),
version: shortUIntConst(1, true),
payerId: address(Encoding.AccountAddress),
nonce: nonce('payerId'),
fee,
tx: entry(),
tx: entryAny,
}] as const;

type TxSchema = SchemaTypes<typeof txSchema>;
Expand Down
15 changes: 12 additions & 3 deletions src/tx/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Encoded, decode } from '../utils/encoder';
import Node, { TransformNodeType } from '../Node';
import { Account } from '../apis/node';
import { genAggressiveCacheGetResponsesPolicy } from '../utils/autorest';
import { UnexpectedTsError } from '../utils/errors';
import { ArgumentError, UnexpectedTsError } from '../utils/errors';

export interface ValidatorResult {
message: string;
Expand Down Expand Up @@ -131,11 +131,20 @@ validators.push(
},
(tx, { account, parentTxTypes }) => {
if (account == null) return [];
let extraFee = '0';
if (tx.tag === Tag.PayingForTx) {
if (tx.tx.tag !== Tag.SignedTx) {
throw new ArgumentError('Payload of PayingForTx', Tag[Tag.SignedTx], Tag[tx.tx.tag]);
}
// TODO: calculate nested tx fee more accurate
if ('fee' in tx.tx.encodedTx) {
extraFee = tx.tx.encodedTx.fee;
}
}
const cost = new BigNumber('fee' in tx ? tx.fee : 0)
.plus('nameFee' in tx ? tx.nameFee : 0)
.plus('amount' in tx ? tx.amount : 0)
// TODO: calculate nested tx fee more accurate
.plus(tx.tag === Tag.PayingForTx ? tx.tx.encodedTx.fee : 0)
.plus(extraFee)
.minus(parentTxTypes.includes(Tag.PayingForTx) && 'fee' in tx ? tx.fee : 0);
if (cost.lte(account.balance.toString())) return [];
return [{
Expand Down
6 changes: 5 additions & 1 deletion test/integration/ga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ describe('Generalized Account', () => {
it('buildAuthTxHash generates a proper hash', async () => {
const { rawTx } = await aeSdk
.spend(10000, publicKey, { authData: { sourceCode, args: [genSalt()] } });
const spendTx = buildTx(unpackTx(rawTx, Tag.SignedTx).encodedTx.tx.encodedTx);
const gaMetaTxParams = unpackTx(rawTx, Tag.SignedTx).encodedTx;
if (gaMetaTxParams.tag !== Tag.GaMetaTx || gaMetaTxParams.tx.tag !== Tag.SignedTx) {
throw new Error('Unexpected nested transaction');
}
const spendTx = buildTx(gaMetaTxParams.tx.encodedTx);
expect(await aeSdk.buildAuthTxHash(spendTx)).to.be
.eql((await authContract.getTxHash()).decodedResult);
});
Expand Down
Loading

0 comments on commit 4f4dff6

Please sign in to comment.