Skip to content

Commit db322fd

Browse files
committed
feat: introduce contract class
to help with contract calls, especially invoking functions
1 parent 46f7173 commit db322fd

File tree

13 files changed

+33222
-250
lines changed

13 files changed

+33222
-250
lines changed

.eslintrc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"node": true,
66
"jest": true
77
},
8-
"extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "plugin:prettier/recommended", ],
8+
"extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "plugin:prettier/recommended"],
99
"globals": {
1010
"Atomics": "readonly",
1111
"SharedArrayBuffer": "readonly"
@@ -16,5 +16,9 @@
1616
"sourceType": "module",
1717
"project": "./tsconfig.eslint.json"
1818
},
19-
"plugins": ["@typescript-eslint"]
19+
"plugins": ["@typescript-eslint"],
20+
"rules": {
21+
"import/prefer-default-export": 0,
22+
"@typescript-eslint/naming-convention": 0
23+
}
2024
}

__mocks__/ERC20.json

Lines changed: 32727 additions & 0 deletions
Large diffs are not rendered by default.

__tests__/contracts.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from 'fs';
2+
import { CompiledContract, Contract, deployContract, JsonParser, randomAddress } from '../src';
3+
4+
const compiledERC20: CompiledContract = JsonParser.parse(
5+
fs.readFileSync('./__mocks__/ERC20.json').toString('ascii')
6+
);
7+
8+
describe('new Contract()', () => {
9+
const address = randomAddress();
10+
// const address = "";
11+
const wallet = randomAddress();
12+
const contract = new Contract(compiledERC20.abi, address);
13+
beforeAll(async () => {
14+
const { code, tx_id } = await deployContract(compiledERC20, address);
15+
// I want to show the tx number to the tester, so he/she can trace the transaction in the explorer.
16+
// eslint-disable-next-line no-console
17+
console.log('deployed erc20 contract', tx_id);
18+
expect(code).toBe('TRANSACTION_RECEIVED');
19+
});
20+
test('initialize ERC20 mock contract', async () => {
21+
const response = await contract.invoke('mint', {
22+
recipient: wallet,
23+
amount: '10',
24+
});
25+
expect(response.code).toBe('TRANSACTION_RECEIVED');
26+
27+
// I want to show the tx number to the tester, so he/she can trace the transaction in the explorer.
28+
// eslint-disable-next-line no-console
29+
console.log('txId:', response.tx_id, ', funded wallet:', wallet);
30+
});
31+
});

__tests__/index.test.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import fs from 'fs';
2-
import starknet, {
2+
import {
33
CompiledContract,
44
compressProgram,
55
randomAddress,
6-
makeAddress,
76
JsonParser,
8-
} from '..';
7+
getContractAddresses,
8+
getBlock,
9+
getCode,
10+
getStorageAt,
11+
getTransactionStatus,
12+
getTransaction,
13+
addTransaction,
14+
deployContract,
15+
} from '../src';
916

1017
const compiledArgentAccount = JsonParser.parse(
1118
fs.readFileSync('./__mocks__/ArgentAccount.json').toString('ascii')
@@ -14,31 +21,27 @@ const compiledArgentAccount = JsonParser.parse(
1421
describe('starknet endpoints', () => {
1522
describe('feeder gateway endpoints', () => {
1623
test('getContractAddresses()', () => {
17-
return expect(starknet.getContractAddresses()).resolves.not.toThrow();
24+
return expect(getContractAddresses()).resolves.not.toThrow();
1825
});
1926
xtest('callContract()', () => {});
2027
test('getBlock()', () => {
21-
return expect(starknet.getBlock(46500)).resolves.not.toThrow();
28+
return expect(getBlock(46500)).resolves.not.toThrow();
2229
});
2330
test('getCode()', () => {
2431
return expect(
25-
starknet.getCode('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 46500)
32+
getCode('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 46500)
2633
).resolves.not.toThrow();
2734
});
2835
test('getStorageAt()', () => {
2936
return expect(
30-
starknet.getStorageAt(
31-
'0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d',
32-
0,
33-
46500
34-
)
37+
getStorageAt('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 0, 46500)
3538
).resolves.not.toThrow();
3639
});
3740
test('getTransactionStatus()', () => {
38-
return expect(starknet.getTransactionStatus(286136)).resolves.not.toThrow();
41+
return expect(getTransactionStatus(286136)).resolves.not.toThrow();
3942
});
4043
test('getTransaction()', () => {
41-
return expect(starknet.getTransaction(286136)).resolves.not.toThrow();
44+
return expect(getTransaction(286136)).resolves.not.toThrow();
4245
});
4346
});
4447

@@ -51,7 +54,7 @@ describe('starknet endpoints', () => {
5154
program: compressProgram(inputContract.program),
5255
};
5356

54-
const response = await starknet.addTransaction({
57+
const response = await addTransaction({
5558
type: 'DEPLOY',
5659
contract_address: randomAddress(),
5760
contract_definition: contractDefinition,
@@ -63,15 +66,11 @@ describe('starknet endpoints', () => {
6366
// eslint-disable-next-line no-console
6467
console.log('txId:', response.tx_id);
6568
});
66-
xtest('type: "INVOKE_FUNCTION"', () => {});
6769

6870
test('deployContract()', async () => {
6971
const inputContract = compiledArgentAccount as unknown as CompiledContract;
7072

71-
const response = await starknet.deployContract(
72-
inputContract,
73-
makeAddress('0x20b5B1b8aFd65F1FCB755a449000cFC4aBCA0D40')
74-
);
73+
const response = await deployContract(inputContract);
7574
expect(response.code).toBe('TRANSACTION_RECEIVED');
7675
expect(response.tx_id).toBeGreaterThan(0);
7776

__tests__/utils.browser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import fs from 'fs';
6-
import { compressProgram, isBrowser, JsonParser } from '..';
6+
import { compressProgram, isBrowser, JsonParser } from '../src';
77

88
const compiledArgentAccount = JsonParser.parse(
99
fs.readFileSync('./__mocks__/ArgentAccount.json').toString('ascii')

__tests__/utils.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ describe('starknetKeccak()', () => {
4444
'0x79dc0da7c54b95f10aa182ad0a46400db63156920adb65eca2654c0945a463'
4545
);
4646
});
47+
test('hash works for value="mint"', () => {
48+
expect(getSelectorFromName('mint')).toBe(
49+
'0x02f0b3c5710379609eb5495f1ecd348cb28167711b73609fe565a72734550354'
50+
);
51+
});
4752
});

package-lock.json

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"typescript": "^4.4.4"
5656
},
5757
"dependencies": {
58+
"@ethersproject/bignumber": "^5.5.0",
5859
"axios": "^0.23.0",
5960
"ethereum-cryptography": "^0.2.0",
6061
"json-bigint": "^1.0.0",

src/contract.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import assert from 'assert';
2+
import { BigNumber } from '@ethersproject/bignumber';
3+
import { Abi } from './types';
4+
import { getSelectorFromName } from './utils';
5+
import { addTransaction } from './starknet';
6+
7+
type Args = { [inputName: string]: string | string[] };
8+
type Calldata = string[];
9+
10+
const parseFelt = (candidate: string): BigNumber => {
11+
try {
12+
return BigNumber.from(candidate);
13+
} catch (e) {
14+
throw Error('Couldnt parse felt');
15+
}
16+
};
17+
18+
const isFelt = (candidate: string): boolean => {
19+
try {
20+
parseFelt(candidate);
21+
return true;
22+
} catch (e) {
23+
return false;
24+
}
25+
};
26+
27+
export class Contract {
28+
connectedTo: string | null = null;
29+
30+
abi: Abi[];
31+
32+
/**
33+
* Contract class to handle contract methods
34+
*
35+
* @param abi - Abi of the contract object
36+
* @param address (optional) - address to connect to
37+
*/
38+
constructor(abi: Abi[], address: string | null = null) {
39+
this.connectedTo = address;
40+
this.abi = abi;
41+
}
42+
43+
public connect(address: string): Contract {
44+
this.connectedTo = address;
45+
return this;
46+
}
47+
48+
private static compileCalldata(args: Args): Calldata {
49+
return Object.values(args).flatMap((value) => {
50+
if (Array.isArray(value))
51+
return [
52+
BigNumber.from(value.length).toString(),
53+
...value.map((x) => BigNumber.from(x).toString()),
54+
];
55+
return BigNumber.from(value).toString();
56+
});
57+
}
58+
59+
public invoke(method: string, args: Args = {}) {
60+
// ensure contract is connected
61+
assert(this.connectedTo !== null, 'contract isnt connected to an address');
62+
63+
// ensure provided method exists
64+
const invokeableFunctionNames = this.abi
65+
.filter((abi) => {
66+
const isView = abi.stateMutability === 'view';
67+
const isFunction = abi.type === 'function';
68+
return isFunction && !isView;
69+
})
70+
.map((abi) => abi.name);
71+
assert(invokeableFunctionNames.includes(method), 'invokeable method not found in abi');
72+
73+
// ensure args match abi type
74+
const methodAbi = this.abi.find((abi) => abi.name === method)!;
75+
methodAbi.inputs.forEach((input) => {
76+
assert(args[input.name] !== undefined, `no arg for "${input.name}" provided`);
77+
if (input.type === 'felt') {
78+
assert(typeof args[input.name] === 'string', `arg ${input.name} should be a felt (string)`);
79+
assert(
80+
isFelt(args[input.name] as string),
81+
`arg ${input.name} should be decimal or hexadecimal`
82+
);
83+
} else {
84+
assert(Array.isArray(args[input.name]), `arg ${input.name} should be a felt* (string[])`);
85+
(args[input.name] as string[]).forEach((felt, i) => {
86+
assert(
87+
typeof felt === 'string',
88+
`arg ${input.name}[${i}] should be a felt (string) as part of a felt* (string[])`
89+
);
90+
assert(
91+
isFelt(felt),
92+
`arg ${input.name}[${i}] should be decimal or hexadecimal as part of a felt* (string[])`
93+
);
94+
});
95+
}
96+
});
97+
98+
// compile calldata
99+
const entrypointSelector = getSelectorFromName(method);
100+
const calldata = Contract.compileCalldata(args);
101+
102+
return addTransaction({
103+
type: 'INVOKE_FUNCTION',
104+
contract_address: this.connectedTo,
105+
calldata,
106+
entry_point_selector: entrypointSelector,
107+
});
108+
}
109+
}

0 commit comments

Comments
 (0)