Skip to content

Commit

Permalink
feat: introduce contract class
Browse files Browse the repository at this point in the history
to help with contract calls, especially invoking functions
  • Loading branch information
janek26 committed Oct 25, 2021
1 parent 46f7173 commit db322fd
Show file tree
Hide file tree
Showing 13 changed files with 33,222 additions and 250 deletions.
8 changes: 6 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"node": true,
"jest": true
},
"extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "plugin:prettier/recommended", ],
"extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "plugin:prettier/recommended"],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
Expand All @@ -16,5 +16,9 @@
"sourceType": "module",
"project": "./tsconfig.eslint.json"
},
"plugins": ["@typescript-eslint"]
"plugins": ["@typescript-eslint"],
"rules": {
"import/prefer-default-export": 0,
"@typescript-eslint/naming-convention": 0
}
}
32,727 changes: 32,727 additions & 0 deletions __mocks__/ERC20.json

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions __tests__/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'fs';
import { CompiledContract, Contract, deployContract, JsonParser, randomAddress } from '../src';

const compiledERC20: CompiledContract = JsonParser.parse(
fs.readFileSync('./__mocks__/ERC20.json').toString('ascii')
);

describe('new Contract()', () => {
const address = randomAddress();
// const address = "";
const wallet = randomAddress();
const contract = new Contract(compiledERC20.abi, address);
beforeAll(async () => {
const { code, tx_id } = await deployContract(compiledERC20, address);
// 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('deployed erc20 contract', tx_id);
expect(code).toBe('TRANSACTION_RECEIVED');
});
test('initialize ERC20 mock contract', async () => {
const response = await contract.invoke('mint', {
recipient: wallet,
amount: '10',
});
expect(response.code).toBe('TRANSACTION_RECEIVED');

// 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);
});
});
37 changes: 18 additions & 19 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import fs from 'fs';
import starknet, {
import {
CompiledContract,
compressProgram,
randomAddress,
makeAddress,
JsonParser,
} from '..';
getContractAddresses,
getBlock,
getCode,
getStorageAt,
getTransactionStatus,
getTransaction,
addTransaction,
deployContract,
} from '../src';

const compiledArgentAccount = JsonParser.parse(
fs.readFileSync('./__mocks__/ArgentAccount.json').toString('ascii')
Expand All @@ -14,31 +21,27 @@ const compiledArgentAccount = JsonParser.parse(
describe('starknet endpoints', () => {
describe('feeder gateway endpoints', () => {
test('getContractAddresses()', () => {
return expect(starknet.getContractAddresses()).resolves.not.toThrow();
return expect(getContractAddresses()).resolves.not.toThrow();
});
xtest('callContract()', () => {});
test('getBlock()', () => {
return expect(starknet.getBlock(46500)).resolves.not.toThrow();
return expect(getBlock(46500)).resolves.not.toThrow();
});
test('getCode()', () => {
return expect(
starknet.getCode('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 46500)
getCode('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 46500)
).resolves.not.toThrow();
});
test('getStorageAt()', () => {
return expect(
starknet.getStorageAt(
'0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d',
0,
46500
)
getStorageAt('0x5f778a983bf8760ad37868f4c869d70247c5546044a7f0386df96d8043d4e9d', 0, 46500)
).resolves.not.toThrow();
});
test('getTransactionStatus()', () => {
return expect(starknet.getTransactionStatus(286136)).resolves.not.toThrow();
return expect(getTransactionStatus(286136)).resolves.not.toThrow();
});
test('getTransaction()', () => {
return expect(starknet.getTransaction(286136)).resolves.not.toThrow();
return expect(getTransaction(286136)).resolves.not.toThrow();
});
});

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

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

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

const response = await starknet.deployContract(
inputContract,
makeAddress('0x20b5B1b8aFd65F1FCB755a449000cFC4aBCA0D40')
);
const response = await deployContract(inputContract);
expect(response.code).toBe('TRANSACTION_RECEIVED');
expect(response.tx_id).toBeGreaterThan(0);

Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import fs from 'fs';
import { compressProgram, isBrowser, JsonParser } from '..';
import { compressProgram, isBrowser, JsonParser } from '../src';

const compiledArgentAccount = JsonParser.parse(
fs.readFileSync('./__mocks__/ArgentAccount.json').toString('ascii')
Expand Down
5 changes: 5 additions & 0 deletions __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ describe('starknetKeccak()', () => {
'0x79dc0da7c54b95f10aa182ad0a46400db63156920adb65eca2654c0945a463'
);
});
test('hash works for value="mint"', () => {
expect(getSelectorFromName('mint')).toBe(
'0x02f0b3c5710379609eb5495f1ecd348cb28167711b73609fe565a72734550354'
);
});
});
87 changes: 87 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"typescript": "^4.4.4"
},
"dependencies": {
"@ethersproject/bignumber": "^5.5.0",
"axios": "^0.23.0",
"ethereum-cryptography": "^0.2.0",
"json-bigint": "^1.0.0",
Expand Down
109 changes: 109 additions & 0 deletions src/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import assert from 'assert';
import { BigNumber } from '@ethersproject/bignumber';
import { Abi } from './types';
import { getSelectorFromName } from './utils';
import { addTransaction } from './starknet';

type Args = { [inputName: string]: string | string[] };
type Calldata = string[];

const parseFelt = (candidate: string): BigNumber => {
try {
return BigNumber.from(candidate);
} catch (e) {
throw Error('Couldnt parse felt');
}
};

const isFelt = (candidate: string): boolean => {
try {
parseFelt(candidate);
return true;
} catch (e) {
return false;
}
};

export class Contract {
connectedTo: string | null = null;

abi: Abi[];

/**
* Contract class to handle contract methods
*
* @param abi - Abi of the contract object
* @param address (optional) - address to connect to
*/
constructor(abi: Abi[], address: string | null = null) {
this.connectedTo = address;
this.abi = abi;
}

public connect(address: string): Contract {
this.connectedTo = address;
return this;
}

private static compileCalldata(args: Args): Calldata {
return Object.values(args).flatMap((value) => {
if (Array.isArray(value))
return [
BigNumber.from(value.length).toString(),
...value.map((x) => BigNumber.from(x).toString()),
];
return BigNumber.from(value).toString();
});
}

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

// ensure provided method exists
const invokeableFunctionNames = this.abi
.filter((abi) => {
const isView = abi.stateMutability === 'view';
const isFunction = abi.type === 'function';
return isFunction && !isView;
})
.map((abi) => abi.name);
assert(invokeableFunctionNames.includes(method), 'invokeable method not found in abi');

// ensure args match abi type
const methodAbi = this.abi.find((abi) => abi.name === method)!;
methodAbi.inputs.forEach((input) => {
assert(args[input.name] !== undefined, `no arg for "${input.name}" provided`);
if (input.type === 'felt') {
assert(typeof args[input.name] === 'string', `arg ${input.name} should be a felt (string)`);
assert(
isFelt(args[input.name] as string),
`arg ${input.name} should be decimal or hexadecimal`
);
} else {
assert(Array.isArray(args[input.name]), `arg ${input.name} should be a felt* (string[])`);
(args[input.name] as string[]).forEach((felt, i) => {
assert(
typeof felt === 'string',
`arg ${input.name}[${i}] should be a felt (string) as part of a felt* (string[])`
);
assert(
isFelt(felt),
`arg ${input.name}[${i}] should be decimal or hexadecimal as part of a felt* (string[])`
);
});
}
});

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

return addTransaction({
type: 'INVOKE_FUNCTION',
contract_address: this.connectedTo,
calldata,
entry_point_selector: entrypointSelector,
});
}
}
Loading

0 comments on commit db322fd

Please sign in to comment.