Skip to content

Commit 10c7fc4

Browse files
committed
feat: type and use callContract
closes #6
1 parent db322fd commit 10c7fc4

File tree

5 files changed

+102
-16
lines changed

5 files changed

+102
-16
lines changed

__tests__/contracts.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
import { BigNumber } from '@ethersproject/bignumber';
12
import fs from 'fs';
2-
import { CompiledContract, Contract, deployContract, JsonParser, randomAddress } from '../src';
3+
import {
4+
CompiledContract,
5+
Contract,
6+
deployContract,
7+
JsonParser,
8+
randomAddress,
9+
waitForTx,
10+
} from '../src';
311

412
const compiledERC20: CompiledContract = JsonParser.parse(
513
fs.readFileSync('./__mocks__/ERC20.json').toString('ascii')
@@ -16,8 +24,15 @@ describe('new Contract()', () => {
1624
// eslint-disable-next-line no-console
1725
console.log('deployed erc20 contract', tx_id);
1826
expect(code).toBe('TRANSACTION_RECEIVED');
27+
await waitForTx(tx_id);
1928
});
20-
test('initialize ERC20 mock contract', async () => {
29+
test('read initial balance of that account', async () => {
30+
const response = await contract.call('balance_of', {
31+
user: wallet,
32+
});
33+
expect(BigNumber.from(response.res)).toStrictEqual(BigNumber.from(0));
34+
});
35+
test('add 10 test ERC20 to account', async () => {
2136
const response = await contract.invoke('mint', {
2237
recipient: wallet,
2338
amount: '10',
@@ -27,5 +42,13 @@ describe('new Contract()', () => {
2742
// I want to show the tx number to the tester, so he/she can trace the transaction in the explorer.
2843
// eslint-disable-next-line no-console
2944
console.log('txId:', response.tx_id, ', funded wallet:', wallet);
45+
await waitForTx(response.tx_id);
46+
});
47+
test('read balance after mint of that account', async () => {
48+
const response = await contract.call('balance_of', {
49+
user: wallet,
50+
});
51+
52+
expect(BigNumber.from(response.res)).toStrictEqual(BigNumber.from(10));
3053
});
3154
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@
6666
"*.{ts,js,md,yml,json}": "prettier --write"
6767
},
6868
"jest": {
69-
"testTimeout": 20000
69+
"testTimeout": 300000
7070
}
7171
}

src/contract.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from 'assert';
22
import { BigNumber } from '@ethersproject/bignumber';
33
import { Abi } from './types';
44
import { getSelectorFromName } from './utils';
5-
import { addTransaction } from './starknet';
5+
import { addTransaction, callContract } from './starknet';
66

77
type Args = { [inputName: string]: string | string[] };
88
type Calldata = string[];
@@ -56,19 +56,19 @@ export class Contract {
5656
});
5757
}
5858

59-
public invoke(method: string, args: Args = {}) {
60-
// ensure contract is connected
61-
assert(this.connectedTo !== null, 'contract isnt connected to an address');
62-
59+
private validateMethodAndArgs(type: 'INVOKE' | 'CALL', method: string, args: Args = {}) {
6360
// ensure provided method exists
6461
const invokeableFunctionNames = this.abi
6562
.filter((abi) => {
6663
const isView = abi.stateMutability === 'view';
6764
const isFunction = abi.type === 'function';
68-
return isFunction && !isView;
65+
return isFunction && type === 'INVOKE' ? !isView : isView;
6966
})
7067
.map((abi) => abi.name);
71-
assert(invokeableFunctionNames.includes(method), 'invokeable method not found in abi');
68+
assert(
69+
invokeableFunctionNames.includes(method),
70+
`${type === 'INVOKE' ? 'invokeable' : 'viewable'} method not found in abi`
71+
);
7272

7373
// ensure args match abi type
7474
const methodAbi = this.abi.find((abi) => abi.name === method)!;
@@ -94,6 +94,24 @@ export class Contract {
9494
});
9595
}
9696
});
97+
}
98+
99+
private parseResponse(method: string, response: (string | string[])[]): Args {
100+
const methodAbi = this.abi.find((abi) => abi.name === method)!;
101+
return methodAbi.outputs.reduce((acc, output, i) => {
102+
return {
103+
...acc,
104+
[output.name]: response[i],
105+
};
106+
}, {} as Args);
107+
}
108+
109+
public invoke(method: string, args: Args = {}) {
110+
// ensure contract is connected
111+
assert(this.connectedTo !== null, 'contract isnt connected to an address');
112+
113+
// validate method and args
114+
this.validateMethodAndArgs('INVOKE', method, args);
97115

98116
// compile calldata
99117
const entrypointSelector = getSelectorFromName(method);
@@ -106,4 +124,22 @@ export class Contract {
106124
entry_point_selector: entrypointSelector,
107125
});
108126
}
127+
128+
public async call(method: string, args: Args = {}) {
129+
// ensure contract is connected
130+
assert(this.connectedTo !== null, 'contract isnt connected to an address');
131+
132+
// validate method and args
133+
this.validateMethodAndArgs('CALL', method, args);
134+
135+
// compile calldata
136+
const entrypointSelector = getSelectorFromName(method);
137+
const calldata = Contract.compileCalldata(args);
138+
139+
return callContract({
140+
contract_address: this.connectedTo,
141+
calldata,
142+
entry_point_selector: entrypointSelector,
143+
}).then((x) => this.parseResponse(method, x.result));
144+
}
109145
}

src/starknet.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
Transaction,
1010
AddTransactionResponse,
1111
CompiledContract,
12+
Call,
13+
CallContractResponse,
1214
} from './types';
1315

1416
const API_URL = 'https://alpha2.starknet.io';
@@ -32,20 +34,19 @@ export function getContractAddresses(): Promise<GetContractAddressesResponse> {
3234
});
3335
}
3436

35-
// TODO: add proper type
3637
/**
3738
* Calls a function on the StarkNet contract.
3839
*
3940
* [Reference](https://github.com/starkware-libs/cairo-lang/blob/f464ec4797361b6be8989e36e02ec690e74ef285/src/starkware/starknet/services/api/feeder_gateway/feeder_gateway_client.py#L17-L25)
4041
*
41-
* @param invokeTx - transaction to be invoked (WIP)
42+
* @param invokeTx - transaction to be invoked
4243
* @param blockId
4344
* @returns the result of the function on the smart contract.
4445
*/
45-
export function callContract(invokeTx: object, blockId: number): Promise<object> {
46+
export function callContract(invokeTx: Call, blockId?: number): Promise<CallContractResponse> {
4647
return new Promise((resolve, reject) => {
4748
axios
48-
.post(`${FEEDER_GATEWAY_URL}/call_contract?blockId=${blockId}`, invokeTx)
49+
.post(`${FEEDER_GATEWAY_URL}/call_contract?blockId=${blockId ?? 'null'}`, invokeTx)
4950
.then((resp: any) => {
5051
resolve(resp.data);
5152
})
@@ -167,7 +168,7 @@ export function getTransaction(txId: number): Promise<GetTransactionResponse> {
167168
*
168169
* [Reference](https://github.com/starkware-libs/cairo-lang/blob/f464ec4797361b6be8989e36e02ec690e74ef285/src/starkware/starknet/services/api/gateway/gateway_client.py#L13-L17)
169170
*
170-
* @param tx - transaction to be invoked (WIP)
171+
* @param tx - transaction to be invoked
171172
* @returns a confirmation of invoking a function on the starknet contract
172173
*/
173174
export function addTransaction(tx: Transaction): Promise<AddTransactionResponse> {
@@ -206,7 +207,27 @@ export function deployContract(
206207
});
207208
}
208209

210+
const wait = (delay: number) => new Promise((res) => setTimeout(res, delay));
211+
export async function waitForTx(txId: number, retryInterval: number = 2000) {
212+
let onchain = false;
213+
while (!onchain) {
214+
// eslint-disable-next-line no-await-in-loop
215+
const res = await getTransactionStatus(txId);
216+
if (res.tx_status === 'ACCEPTED_ONCHAIN' || res.tx_status === 'PENDING') {
217+
onchain = true;
218+
} else if (res.tx_status === 'REJECTED') {
219+
throw Error('REJECTED');
220+
} else if (res.tx_status === 'NOT_RECEIVED') {
221+
throw Error('NOT_RECEIVED');
222+
} else {
223+
// eslint-disable-next-line no-await-in-loop
224+
await wait(retryInterval);
225+
}
226+
}
227+
}
228+
209229
export default {
230+
waitForTx,
210231
getContractAddresses,
211232
callContract,
212233
getBlock,

src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type CompressedProgram = string;
1212
export interface Abi {
1313
inputs: { name: string; type: 'felt' | 'felt*' }[];
1414
name: string;
15-
outputs: { name: string; type: string }[];
15+
outputs: { name: string; type: 'felt' | 'felt*' }[];
1616
stateMutability?: 'view';
1717
type: 'function';
1818
}
@@ -43,8 +43,14 @@ export interface InvokeFunctionTransaction {
4343
calldata?: string[];
4444
}
4545

46+
export type Call = Omit<InvokeFunctionTransaction, 'type'>;
47+
4648
export type Transaction = DeployTransaction | InvokeFunctionTransaction;
4749

50+
export interface CallContractResponse {
51+
result: string[];
52+
}
53+
4854
export interface GetBlockResponse {
4955
sequence_number: number;
5056
state_root: string;

0 commit comments

Comments
 (0)