Skip to content

Commit

Permalink
feat: type and use callContract
Browse files Browse the repository at this point in the history
closes #6
  • Loading branch information
janek26 committed Oct 25, 2021
1 parent db322fd commit 10c7fc4
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 16 deletions.
27 changes: 25 additions & 2 deletions __tests__/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { BigNumber } from '@ethersproject/bignumber';
import fs from 'fs';
import { CompiledContract, Contract, deployContract, JsonParser, randomAddress } from '../src';
import {
CompiledContract,
Contract,
deployContract,
JsonParser,
randomAddress,
waitForTx,
} from '../src';

const compiledERC20: CompiledContract = JsonParser.parse(
fs.readFileSync('./__mocks__/ERC20.json').toString('ascii')
Expand All @@ -16,8 +24,15 @@ describe('new Contract()', () => {
// eslint-disable-next-line no-console
console.log('deployed erc20 contract', tx_id);
expect(code).toBe('TRANSACTION_RECEIVED');
await waitForTx(tx_id);
});
test('initialize ERC20 mock contract', async () => {
test('read initial balance of that account', async () => {
const response = await contract.call('balance_of', {
user: wallet,
});
expect(BigNumber.from(response.res)).toStrictEqual(BigNumber.from(0));
});
test('add 10 test ERC20 to account', async () => {
const response = await contract.invoke('mint', {
recipient: wallet,
amount: '10',
Expand All @@ -27,5 +42,13 @@ describe('new Contract()', () => {
// I want to show the tx number to the tester, so he/she can trace the transaction in the explorer.
// eslint-disable-next-line no-console
console.log('txId:', response.tx_id, ', funded wallet:', wallet);
await waitForTx(response.tx_id);
});
test('read balance after mint of that account', async () => {
const response = await contract.call('balance_of', {
user: wallet,
});

expect(BigNumber.from(response.res)).toStrictEqual(BigNumber.from(10));
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@
"*.{ts,js,md,yml,json}": "prettier --write"
},
"jest": {
"testTimeout": 20000
"testTimeout": 300000
}
}
50 changes: 43 additions & 7 deletions src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from 'assert';
import { BigNumber } from '@ethersproject/bignumber';
import { Abi } from './types';
import { getSelectorFromName } from './utils';
import { addTransaction } from './starknet';
import { addTransaction, callContract } from './starknet';

type Args = { [inputName: string]: string | string[] };
type Calldata = string[];
Expand Down Expand Up @@ -56,19 +56,19 @@ export class Contract {
});
}

public invoke(method: string, args: Args = {}) {
// ensure contract is connected
assert(this.connectedTo !== null, 'contract isnt connected to an address');

private validateMethodAndArgs(type: 'INVOKE' | 'CALL', method: string, args: Args = {}) {
// ensure provided method exists
const invokeableFunctionNames = this.abi
.filter((abi) => {
const isView = abi.stateMutability === 'view';
const isFunction = abi.type === 'function';
return isFunction && !isView;
return isFunction && type === 'INVOKE' ? !isView : isView;
})
.map((abi) => abi.name);
assert(invokeableFunctionNames.includes(method), 'invokeable method not found in abi');
assert(
invokeableFunctionNames.includes(method),
`${type === 'INVOKE' ? 'invokeable' : 'viewable'} method not found in abi`
);

// ensure args match abi type
const methodAbi = this.abi.find((abi) => abi.name === method)!;
Expand All @@ -94,6 +94,24 @@ export class Contract {
});
}
});
}

private parseResponse(method: string, response: (string | string[])[]): Args {
const methodAbi = this.abi.find((abi) => abi.name === method)!;
return methodAbi.outputs.reduce((acc, output, i) => {
return {
...acc,
[output.name]: response[i],
};
}, {} as Args);
}

public invoke(method: string, args: Args = {}) {
// ensure contract is connected
assert(this.connectedTo !== null, 'contract isnt connected to an address');

// validate method and args
this.validateMethodAndArgs('INVOKE', method, args);

// compile calldata
const entrypointSelector = getSelectorFromName(method);
Expand All @@ -106,4 +124,22 @@ export class Contract {
entry_point_selector: entrypointSelector,
});
}

public async call(method: string, args: Args = {}) {
// ensure contract is connected
assert(this.connectedTo !== null, 'contract isnt connected to an address');

// validate method and args
this.validateMethodAndArgs('CALL', method, args);

// compile calldata
const entrypointSelector = getSelectorFromName(method);
const calldata = Contract.compileCalldata(args);

return callContract({
contract_address: this.connectedTo,
calldata,
entry_point_selector: entrypointSelector,
}).then((x) => this.parseResponse(method, x.result));
}
}
31 changes: 26 additions & 5 deletions src/starknet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
Transaction,
AddTransactionResponse,
CompiledContract,
Call,
CallContractResponse,
} from './types';

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

// TODO: add proper type
/**
* Calls a function on the StarkNet contract.
*
* [Reference](https://github.com/starkware-libs/cairo-lang/blob/f464ec4797361b6be8989e36e02ec690e74ef285/src/starkware/starknet/services/api/feeder_gateway/feeder_gateway_client.py#L17-L25)
*
* @param invokeTx - transaction to be invoked (WIP)
* @param invokeTx - transaction to be invoked
* @param blockId
* @returns the result of the function on the smart contract.
*/
export function callContract(invokeTx: object, blockId: number): Promise<object> {
export function callContract(invokeTx: Call, blockId?: number): Promise<CallContractResponse> {
return new Promise((resolve, reject) => {
axios
.post(`${FEEDER_GATEWAY_URL}/call_contract?blockId=${blockId}`, invokeTx)
.post(`${FEEDER_GATEWAY_URL}/call_contract?blockId=${blockId ?? 'null'}`, invokeTx)
.then((resp: any) => {
resolve(resp.data);
})
Expand Down Expand Up @@ -167,7 +168,7 @@ export function getTransaction(txId: number): Promise<GetTransactionResponse> {
*
* [Reference](https://github.com/starkware-libs/cairo-lang/blob/f464ec4797361b6be8989e36e02ec690e74ef285/src/starkware/starknet/services/api/gateway/gateway_client.py#L13-L17)
*
* @param tx - transaction to be invoked (WIP)
* @param tx - transaction to be invoked
* @returns a confirmation of invoking a function on the starknet contract
*/
export function addTransaction(tx: Transaction): Promise<AddTransactionResponse> {
Expand Down Expand Up @@ -206,7 +207,27 @@ export function deployContract(
});
}

const wait = (delay: number) => new Promise((res) => setTimeout(res, delay));
export async function waitForTx(txId: number, retryInterval: number = 2000) {
let onchain = false;
while (!onchain) {
// eslint-disable-next-line no-await-in-loop
const res = await getTransactionStatus(txId);
if (res.tx_status === 'ACCEPTED_ONCHAIN' || res.tx_status === 'PENDING') {
onchain = true;
} else if (res.tx_status === 'REJECTED') {
throw Error('REJECTED');
} else if (res.tx_status === 'NOT_RECEIVED') {
throw Error('NOT_RECEIVED');
} else {
// eslint-disable-next-line no-await-in-loop
await wait(retryInterval);
}
}
}

export default {
waitForTx,
getContractAddresses,
callContract,
getBlock,
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type CompressedProgram = string;
export interface Abi {
inputs: { name: string; type: 'felt' | 'felt*' }[];
name: string;
outputs: { name: string; type: string }[];
outputs: { name: string; type: 'felt' | 'felt*' }[];
stateMutability?: 'view';
type: 'function';
}
Expand Down Expand Up @@ -43,8 +43,14 @@ export interface InvokeFunctionTransaction {
calldata?: string[];
}

export type Call = Omit<InvokeFunctionTransaction, 'type'>;

export type Transaction = DeployTransaction | InvokeFunctionTransaction;

export interface CallContractResponse {
result: string[];
}

export interface GetBlockResponse {
sequence_number: number;
state_root: string;
Expand Down

0 comments on commit 10c7fc4

Please sign in to comment.