Skip to content

Commit

Permalink
fix: correctly parse structs in Starknet return types
Browse files Browse the repository at this point in the history
Currently, starknet.js doesn't do a good job with structs, such as the Uint256 struct returned by the ERC20 contract. Instead of returning all the fields of the struct, it just returns the first field.

Concretely, if a user had a balance of 1 in an ERC20 contract, this is what would have been returned from `balance_of` previously:

```json
{ "res": "0x0" }
```

With this change we properly parse the Uint256 struct, so the method will instead return the following:

```json
{ "res": { "low": "0x1", "high": "0x0" } }
```
  • Loading branch information
corbt committed Nov 21, 2021
1 parent 0a302e1 commit 5a4a318
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 14 deletions.
53 changes: 42 additions & 11 deletions src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BN from 'bn.js';
import assert from 'minimalistic-assert';

import { Provider, defaultProvider } from './provider';
import { Abi } from './types';
import { Abi, AbiEntry, FunctionAbi, StructAbi } from './types';
import { BigNumberish, toBN } from './utils/number';
import { getSelectorFromName } from './utils/stark';

Expand Down Expand Up @@ -39,6 +39,8 @@ export class Contract {

abi: Abi[];

structs: { [name: string]: StructAbi };

provider: Provider;

/**
Expand All @@ -51,6 +53,15 @@ export class Contract {
this.connectedTo = address;
this.provider = provider;
this.abi = abi;
this.structs = abi
.filter((abiEntry) => abiEntry.type === 'struct')
.reduce(
(acc, abiEntry) => ({
...acc,
[abiEntry.name]: abiEntry,
}),
{}
);
}

public connect(address: string): Contract {
Expand All @@ -62,9 +73,9 @@ export class Contract {
// ensure provided method exists
const invokeableFunctionNames = this.abi
.filter((abi) => {
if (abi.type !== 'function') return false;
const isView = abi.stateMutability === 'view';
const isFunction = abi.type === 'function';
return isFunction && type === 'INVOKE' ? !isView : isView;
return type === 'INVOKE' ? !isView : isView;
})
.map((abi) => abi.name);
assert(
Expand All @@ -73,7 +84,9 @@ export class Contract {
);

// ensure args match abi type
const methodAbi = this.abi.find((abi) => abi.name === method)!;
const methodAbi = this.abi.find(
(abi) => abi.name === method && abi.type === 'function'
) as FunctionAbi;
methodAbi.inputs.forEach((input) => {
if (args[input.name] !== undefined) {
if (input.type === 'felt') {
Expand Down Expand Up @@ -102,14 +115,32 @@ 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 {
private parseResponseField(
element: AbiEntry | FunctionAbi,
responseIterator: Iterator<string>
): Args {
let entries: AbiEntry[] = [];
if (['felt', 'felt*'].includes(element.type)) {
return responseIterator.next().value;
}
if (element.type in this.structs) {
entries = this.structs[element.type].members;
} else if ('outputs' in element) {
entries = element.outputs;
}
return entries.reduce(
(acc, member) => ({
...acc,
[output.name]: response[i],
};
}, {} as Args);
[member.name]: this.parseResponseField(member, responseIterator),
}),
{}
);
}

private parseResponse(method: string, response: string[]): Args {
const methodAbi = this.abi.find((abi) => abi.name === method) as FunctionAbi;
const responseIterator = response.flat()[Symbol.iterator]();
return this.parseResponseField(methodAbi, responseIterator);
}

public invoke(method: string, args: Args = {}, signature?: [BigNumberish, BigNumberish]) {
Expand Down
18 changes: 15 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,25 @@ export type Type = 'DEPLOY' | 'INVOKE_FUNCTION';
export type EntryPointType = 'EXTERNAL';
export type CompressedProgram = string;

export type Abi = {
inputs: { name: string; type: 'felt' | 'felt*' }[];
export type AbiEntry = { name: string; type: 'felt' | 'felt*' | string };

export type FunctionAbi = {
inputs: AbiEntry[];
name: string;
outputs: { name: string; type: 'felt' | 'felt*' }[];
outputs: AbiEntry[];
stateMutability?: 'view';
type: 'function';
};

export type StructAbi = {
members: { name: string; offset: number; type: 'felt' | 'felt*' | string }[];
name: string;
size: number;
type: 'struct';
};

export type Abi = FunctionAbi | StructAbi;

export type EntryPointsByType = object;
export type Program = object;

Expand Down

0 comments on commit 5a4a318

Please sign in to comment.