Skip to content

Commit 92f67ce

Browse files
jasonpaulosaldur
authored andcommitted
Add ABI interaction support (#466)
* Move multisig code out of main * Implement JSON description objects * Fix multisig export * Implement atomic txn composer and fix variable shadow issues * Actually copy transactions * Implement JSON description cucumber tests * Fix lots of bugs and pass JSON cucumber tests * Implement payment txn encoding test * Implement composer tests * Add missing export to main * Fix step implementation * Fix remaining integration tests * Fix for firefox * Final fixes for firefox * appID -> appId * Actually finally fix firefox tests * Check for unpaired parenthesis when parsing method sig * Also allow generic `txn` type * Handle null method args * Throw error when building a group with 0 txns * PR feedback
1 parent d42d385 commit 92f67ce

File tree

18 files changed

+1537
-163
lines changed

18 files changed

+1537
-163
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
},
2525
],
2626
'import/prefer-default-export': 'off',
27+
'no-continue': 'off',
2728
'lines-between-class-members': [
2829
'error',
2930
'always',
@@ -33,6 +34,8 @@ module.exports = {
3334
'@typescript-eslint/no-unused-vars': ['error'],
3435
'no-redeclare': 'off',
3536
'@typescript-eslint/no-redeclare': ['error'],
37+
'no-shadow': 'off',
38+
'@typescript-eslint/no-shadow': ['error'],
3639
},
3740
overrides: [
3841
{

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
unit:
2-
node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.responses.231 or @unit.feetest or @unit.indexer.logs" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
2+
node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.transactions.payment or @unit.responses.231 or @unit.feetest or @unit.indexer.logs or @unit.abijson or @unit.atomic_transaction_composer" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
33
integration:
4-
node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @template or @indexer or @rekey or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
4+
node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @template or @indexer or @rekey or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
55

66
docker-test:
77
./tests/cucumber/docker/run_docker.sh

src/abi/abi_type.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ interface Segment {
3131
const staticArrayRegexp = /^([a-z\d[\](),]+)\[([1-9][\d]*)]$/;
3232
const ufixedRegexp = /^ufixed([1-9][\d]*)x([1-9][\d]*)$/;
3333

34-
type ABIValue = boolean | number | bigint | string | Uint8Array | ABIValue[];
34+
export type ABIValue =
35+
| boolean
36+
| number
37+
| bigint
38+
| string
39+
| Uint8Array
40+
| ABIValue[];
3541

3642
export abstract class ABIType {
3743
// Converts a ABIType object to a string
@@ -691,28 +697,26 @@ export class ABITupleType extends ABIType {
691697

692698
// Check segment indices are valid
693699
// If the dynamic segment are not consecutive and well-ordered, we return error
694-
// eslint-disable-next-line no-shadow
695-
for (let i = 0; i < dynamicSegments.length; i++) {
696-
const seg = dynamicSegments[i];
700+
for (let j = 0; j < dynamicSegments.length; j++) {
701+
const seg = dynamicSegments[j];
697702
if (seg.left > seg.right) {
698703
throw new Error(
699704
'dynamic segment should display a [l, r] space with l <= r'
700705
);
701706
}
702707
if (
703-
i !== dynamicSegments.length - 1 &&
704-
seg.right !== dynamicSegments[i + 1].left
708+
j !== dynamicSegments.length - 1 &&
709+
seg.right !== dynamicSegments[j + 1].left
705710
) {
706711
throw new Error('dynamic segment should be consecutive');
707712
}
708713
}
709714

710715
// Check dynamic element partitions
711716
let segIndex = 0;
712-
// eslint-disable-next-line no-shadow
713-
for (let i = 0; i < tupleTypes.length; i++) {
714-
if (tupleTypes[i].isDynamic()) {
715-
valuePartition[i] = byteString.slice(
717+
for (let j = 0; j < tupleTypes.length; j++) {
718+
if (tupleTypes[j].isDynamic()) {
719+
valuePartition[j] = byteString.slice(
716720
dynamicSegments[segIndex].left,
717721
dynamicSegments[segIndex].right
718722
);
@@ -722,9 +726,8 @@ export class ABITupleType extends ABIType {
722726

723727
// Decode each tuple element
724728
const returnValues: ABIValue[] = [];
725-
// eslint-disable-next-line no-shadow
726-
for (let i = 0; i < tupleTypes.length; i++) {
727-
const valueTi = tupleTypes[i].decode(valuePartition[i]);
729+
for (let j = 0; j < tupleTypes.length; j++) {
730+
const valueTi = tupleTypes[j].decode(valuePartition[j]);
728731
returnValues.push(valueTi);
729732
}
730733
return returnValues;

src/abi/contract.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ABIMethod, ABIMethodParams } from './method';
2+
3+
export interface ABIContractParams {
4+
name: string;
5+
appId: number;
6+
methods: ABIMethodParams[];
7+
}
8+
9+
export class ABIContract {
10+
public readonly name: string;
11+
public readonly appId: number;
12+
public readonly methods: ABIMethod[];
13+
14+
constructor(params: ABIContractParams) {
15+
if (
16+
typeof params.name !== 'string' ||
17+
typeof params.appId !== 'number' ||
18+
!Array.isArray(params.methods)
19+
) {
20+
throw new Error('Invalid ABIContract parameters');
21+
}
22+
23+
this.name = params.name;
24+
this.appId = params.appId;
25+
this.methods = params.methods.map((method) => new ABIMethod(method));
26+
}
27+
28+
toJSON(): ABIContractParams {
29+
return {
30+
name: this.name,
31+
appId: this.appId,
32+
methods: this.methods.map((method) => method.toJSON()),
33+
};
34+
}
35+
}

src/abi/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './abi_type';
2+
export * from './contract';
3+
export * from './interface';
4+
export * from './method';

src/abi/interface.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ABIMethod, ABIMethodParams } from './method';
2+
3+
export interface ABIInterfaceParams {
4+
name: string;
5+
methods: ABIMethodParams[];
6+
}
7+
8+
export class ABIInterface {
9+
public readonly name: string;
10+
public readonly methods: ABIMethod[];
11+
12+
constructor(params: ABIInterfaceParams) {
13+
if (typeof params.name !== 'string' || !Array.isArray(params.methods)) {
14+
throw new Error('Invalid ABIInterface parameters');
15+
}
16+
17+
this.name = params.name;
18+
this.methods = params.methods.map((method) => new ABIMethod(method));
19+
}
20+
21+
toJSON(): ABIInterfaceParams {
22+
return {
23+
name: this.name,
24+
methods: this.methods.map((method) => method.toJSON()),
25+
};
26+
}
27+
}

src/abi/method.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { genericHash } from '../nacl/naclWrappers';
2+
import { TransactionType } from '../types/transactions/base';
3+
import { ABIType, ABITupleType } from './abi_type';
4+
5+
export function abiTypeIsTransaction(type: string): type is TransactionType {
6+
return type === 'txn' || Object.keys(TransactionType).includes(type);
7+
}
8+
9+
function parseMethodSignature(
10+
signature: string
11+
): { name: string; args: string[]; returns: string } {
12+
const argsStart = signature.indexOf('(');
13+
if (argsStart === -1) {
14+
throw new Error(`Invalid method signature: ${signature}`);
15+
}
16+
17+
let argsEnd = -1;
18+
let depth = 0;
19+
for (let i = argsStart; i < signature.length; i++) {
20+
const char = signature[i];
21+
22+
if (char === '(') {
23+
depth += 1;
24+
} else if (char === ')') {
25+
if (depth === 0) {
26+
// unpaired parenthesis
27+
break;
28+
}
29+
30+
depth -= 1;
31+
if (depth === 0) {
32+
argsEnd = i;
33+
break;
34+
}
35+
}
36+
}
37+
38+
if (argsEnd === -1) {
39+
throw new Error(`Invalid method signature: ${signature}`);
40+
}
41+
42+
return {
43+
name: signature.slice(0, argsStart),
44+
args: ABITupleType.parseTupleContent(
45+
signature.slice(argsStart + 1, argsEnd)
46+
),
47+
returns: signature.slice(argsEnd + 1),
48+
};
49+
}
50+
51+
export interface ABIMethodParams {
52+
name: string;
53+
desc?: string;
54+
args: Array<{ type: string; name?: string; desc?: string }>;
55+
returns: { type: string; desc?: string };
56+
}
57+
58+
export type ABIArgumentType = ABIType | TransactionType;
59+
60+
export type ABIReturnType = ABIType | 'void';
61+
62+
export class ABIMethod {
63+
public readonly name: string;
64+
public readonly description?: string;
65+
public readonly args: Array<{
66+
type: ABIArgumentType;
67+
name?: string;
68+
description?: string;
69+
}>;
70+
71+
public readonly returns: { type: ABIReturnType; description?: string };
72+
73+
constructor(params: ABIMethodParams) {
74+
if (
75+
typeof params.name !== 'string' ||
76+
typeof params.returns !== 'object' ||
77+
!Array.isArray(params.args)
78+
) {
79+
throw new Error('Invalid ABIMethod parameters');
80+
}
81+
82+
this.name = params.name;
83+
this.description = params.desc;
84+
this.args = params.args.map(({ type, name, desc }) => {
85+
if (abiTypeIsTransaction(type)) {
86+
return {
87+
type,
88+
name,
89+
description: desc,
90+
};
91+
}
92+
93+
return {
94+
type: ABIType.from(type),
95+
name,
96+
description: desc,
97+
};
98+
});
99+
this.returns = {
100+
type:
101+
params.returns.type === 'void'
102+
? params.returns.type
103+
: ABIType.from(params.returns.type),
104+
description: params.returns.desc,
105+
};
106+
}
107+
108+
getSignature(): string {
109+
const args = this.args.map((arg) => arg.type.toString()).join(',');
110+
const returns = this.returns.type.toString();
111+
return `${this.name}(${args})${returns}`;
112+
}
113+
114+
getSelector(): Uint8Array {
115+
const hash = genericHash(this.getSignature());
116+
return new Uint8Array(hash.slice(0, 4));
117+
}
118+
119+
txnCount(): number {
120+
let count = 1;
121+
for (const arg of this.args) {
122+
if (typeof arg.type === 'string' && abiTypeIsTransaction(arg.type)) {
123+
count += 1;
124+
}
125+
}
126+
return count;
127+
}
128+
129+
toJSON(): ABIMethodParams {
130+
return {
131+
name: this.name,
132+
desc: this.description,
133+
args: this.args.map(({ type, name, description }) => ({
134+
type: type.toString(),
135+
name,
136+
desc: description,
137+
})),
138+
returns: {
139+
type: this.returns.type.toString(),
140+
desc: this.returns.description,
141+
},
142+
};
143+
}
144+
145+
static fromSignature(signature: string): ABIMethod {
146+
const { name, args, returns } = parseMethodSignature(signature);
147+
148+
return new ABIMethod({
149+
name,
150+
args: args.map((arg) => ({ type: arg })),
151+
returns: { type: returns },
152+
});
153+
}
154+
}

0 commit comments

Comments
 (0)