Skip to content

Commit

Permalink
tx: Allow 0x address when deploying contract. Fix {} abi.
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Nov 4, 2024
1 parent 671b157 commit 731fb38
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 72 deletions.
93 changes: 93 additions & 0 deletions src/_type_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ type Writable<T> = T extends {}
type A = Writable<Uint8Array>;
const _a: A = Uint8Array.from([]);
_a;
// IsEmptyArray
const isEmpty = <T>(a: T): abi.IsEmptyArray<T> => a as any;
assertType<true>(isEmpty([] as const));
assertType<false>(isEmpty([1] as const));
assertType<false>(isEmpty(['a', 2] as const));
assertType<false>(isEmpty(['a']));
assertType<true>(isEmpty([] as unknown as []));
assertType<false>(isEmpty([] as unknown as [number]));
assertType<false>(isEmpty([] as unknown as [string, number]));
assertType<false>(isEmpty([] as unknown as Array<string>));
assertType<false>(isEmpty([] as never[]));
assertType<false>(isEmpty([] as any[]));
assertType<true>(isEmpty([] as unknown as undefined));
assertType<true>(isEmpty(undefined));
const t = [
{
type: 'constructor',
inputs: [{ name: 'a', type: 'uint256' }],
stateMutability: 'nonpayable',
},
];
assertType<false>(isEmpty(t));

// Tests
assertType<P.CoderType<string>>(abi.mapComponent({ type: 'string' } as const));
assertType<P.CoderType<string[]>>(abi.mapComponent({ type: 'string[]' } as const));
Expand Down Expand Up @@ -118,6 +141,37 @@ assertType<{
] as const)
);

assertType<{
lol: {
encodeInput: (v: undefined) => Bytes;
decodeOutput: (b: Bytes) => [Bytes, string];
};
}>(
abi.createContract([
{
name: 'lol',
type: 'function',
outputs: [{ type: 'bytes' }, { type: 'address' }],
},
] as const)
);

assertType<{
lol: {
encodeInput: (v: undefined) => Bytes;
decodeOutput: (b: Bytes) => [Bytes, string];
};
}>(
abi.createContract([
{
name: 'lol',
type: 'function',
inputs: [] as const,
outputs: [{ type: 'bytes' }, { type: 'address' }],
},
] as const)
);

assertType<{
lol: {
encodeInput: (v: [bigint, string]) => Bytes;
Expand Down Expand Up @@ -286,3 +340,42 @@ assertType<{
// e.encodeData('Person', { name: 'test', wallet: 1n }); // should fail
// e.sign({ primaryType: 'Person', message: {name: 'test'}, domain: {} }, ''); // should fail
// e.sign({ primaryType: 'Person', message: {name: 'test', wallet: '', s: 3}, domain: {} }, ''); // should fail

// constructor

abi.deployContract(
[{ type: 'constructor', inputs: [], stateMutability: 'nonpayable' }] as const,
'0x00'
);
abi.deployContract([{ type: 'constructor', stateMutability: 'nonpayable' }] as const, '0x00');
// abi.deployContract(
// [{ type: 'constructor', stateMutability: 'nonpayable' }] as const,
// '0x00',
// undefined
// ); // should fail!

abi.deployContract([{ type: 'constructor', stateMutability: 'nonpayable' }], '0x00', undefined); // if we cannot infer type - it will be 'unknown' (and user forced to provide any argument, undefined is ok)

abi.deployContract(
[
{
type: 'constructor',
inputs: [{ name: 'a', type: 'uint256' }],
stateMutability: 'nonpayable',
},
] as const,
'0x00',
100n
);

abi.deployContract(
[
{
type: 'constructor',
inputs: [{ name: 'a', type: 'uint256' }],
stateMutability: 'nonpayable',
},
],
'0x00',
100n
);
131 changes: 77 additions & 54 deletions src/abi/decoder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { keccak_256 } from '@noble/hashes/sha3';
import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils';
import * as P from 'micro-packed';
import { Web3CallArgs, IWeb3Provider, add0x, strip0x, omit, zip } from '../utils.js';
import { Web3CallArgs, IWeb3Provider, add0x, strip0x, omit, zip, ethHex } from '../utils.js';

/*
There is NO network code in the file. However, a user can pass
Expand Down Expand Up @@ -69,6 +69,9 @@ type Writable<T> = T extends {}
}
: T;
type ArrLike<T> = Array<T> | ReadonlyArray<T>;
export type IsEmptyArray<T> =
T extends ReadonlyArray<any> ? (T['length'] extends 0 ? true : false) : true;

export type Component<T extends string> = {
readonly name?: string;
readonly type: T;
Expand Down Expand Up @@ -191,11 +194,15 @@ export function mapComponent<T extends BaseComponent>(c: T): P.CoderType<MapType
}

// If only one arg -- use as is, otherwise construct tuple by tuple rules
export type ArgsType<T> = T extends [Component<string>]
? MapType<T[0]>
: T extends undefined
? 1
: MapTuple<T>;
export type ArgsType<T extends ReadonlyArray<any> | undefined> =
IsEmptyArray<T> extends true
? undefined // empty arr
: T extends ReadonlyArray<any>
? T['length'] extends 1 // single elm
? MapType<T[0]>
: MapTuple<T>
: MapTuple<T>;

// Because args and output are not tuple
// TODO: try merge with mapComponent
export function mapArgs<T extends ArrLike<Component<string>>>(
Expand All @@ -221,60 +228,47 @@ export type FunctionType = Component<'function'> & {
readonly outputs?: ReadonlyArray<Component<string>>;
};

export type FunctionWithInputs = FunctionType & {
inputs: ReadonlyArray<Component<string>>;
};

export type FunctionWithOutputs = FunctionType & {
outputs: ReadonlyArray<Component<string>>;
};

type ContractMethodDecode<
T extends FunctionType,
O = ArgsType<T['outputs']>,
> = T extends FunctionWithOutputs
? { decodeOutput: (b: Uint8Array) => O }
: {
decodeOutput: (b: Uint8Array) => void;
};
type ContractMethodDecode<T extends FunctionType, O = ArgsType<T['outputs']>> =
IsEmptyArray<T['outputs']> extends true
? {
decodeOutput: (b: Uint8Array) => void;
}
: { decodeOutput: (b: Uint8Array) => O };

type ContractMethodEncode<
T extends FunctionType,
I = ArgsType<T['inputs']>,
> = T extends FunctionWithInputs
? { encodeInput: (v: I) => Uint8Array }
: { encodeInput: () => Uint8Array };
type ContractMethodEncode<T extends FunctionType, I = ArgsType<T['inputs']>> =
IsEmptyArray<T['inputs']> extends true
? { encodeInput: () => Uint8Array }
: { encodeInput: (v: I) => Uint8Array };

type ContractMethodGas<
T extends FunctionType,
I = ArgsType<T['inputs']>,
> = T extends FunctionWithInputs
? { estimateGas: (v: I) => Promise<bigint> }
: { estimateGas: () => Promise<bigint> };
type ContractMethodGas<T extends FunctionType, I = ArgsType<T['inputs']>> =
IsEmptyArray<T['inputs']> extends true
? { estimateGas: () => Promise<bigint> }
: { estimateGas: (v: I) => Promise<bigint> };

type ContractMethodCall<
T extends FunctionType,
I = ArgsType<T['inputs']>,
O = ArgsType<T['outputs']>,
> = T extends FunctionWithInputs
? T extends FunctionWithOutputs
? {
// inputs, outputs
call: (v: I) => Promise<O>;
}
: {
// inputs, no outputs
call: (v: I) => Promise<void>;
}
: T extends FunctionWithOutputs
? {
// no inputs, outputs
call: () => Promise<O>;
}
: {
// no inputs, no outputs
call: () => Promise<void>;
};
> =
IsEmptyArray<T['inputs']> extends true
? IsEmptyArray<T['outputs']> extends true
? {
// no inputs, no outputs
call: () => Promise<void>;
}
: {
// no inputs, outputs
call: () => Promise<O>;
}
: IsEmptyArray<T['outputs']> extends true
? {
// inputs, no outputs
call: (v: I) => Promise<void>;
}
: {
// inputs, outputs
call: (v: I) => Promise<O>;
};

export type ContractMethod<T extends FunctionType> = ContractMethodEncode<T> &
ContractMethodDecode<T>;
Expand Down Expand Up @@ -361,7 +355,7 @@ export function createContract<T extends ArrLike<FnArg>>(
let name = fn.name || 'function';
if (nameCnt[name] > 1) name = fnSignature(fn);
const sh = fnSigHash(fn);
const inputs = fn.inputs ? mapArgs(fn.inputs) : undefined;
const inputs = fn.inputs && fn.inputs.length ? mapArgs(fn.inputs) : undefined;
const outputs = fn.outputs ? mapArgs(fn.outputs) : undefined;
const decodeOutput = (b: Uint8Array) => outputs && outputs.decode(b);
const encodeInput = (v: unknown) =>
Expand All @@ -386,6 +380,35 @@ export function createContract<T extends ArrLike<FnArg>>(
return res as any;
}

type GetCons<T extends ArrLike<FnArg>> = Extract<T[number], { type: 'constructor' }>;
type ConstructorType = Component<'constructor'> & {
readonly inputs?: ReadonlyArray<Component<string>>;
};
type ConsArgs<T extends ConstructorType> =
IsEmptyArray<T['inputs']> extends true ? undefined : ArgsType<T['inputs']>;

export function deployContract<T extends ArrLike<FnArg>>(
abi: T,
bytecodeHex: string,
...args: GetCons<T> extends never
? [args: unknown]
: ConsArgs<GetCons<T>> extends undefined
? []
: [args: ConsArgs<GetCons<T>>]
): string {
const bytecode = ethHex.decode(bytecodeHex);
let consCall;
for (let fn of abi) {
if (fn.type !== 'constructor') continue;
const inputs = fn.inputs && fn.inputs.length ? mapArgs(fn.inputs) : undefined;
if (inputs === undefined && args !== undefined && args.length)
throw new Error('arguments to constructor without any');
consCall = inputs ? inputs.encode(args[0] as any) : new Uint8Array();
}
if (!consCall) throw new Error('constructor not found');
return ethHex.encode(concatBytes(bytecode, consCall));
}

export type EventType = NamedComponent<'event'> & {
readonly inputs: ReadonlyArray<Component<string>>;
};
Expand Down
11 changes: 9 additions & 2 deletions src/abi/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ContractInfo, ContractABI, Decoder, createContract, events } from './decoder.js';
import {
ContractInfo,
ContractABI,
Decoder,
createContract,
deployContract,
events,
} from './decoder.js';
import { default as ERC20 } from './erc20.js';
import { default as ERC721 } from './erc721.js';
import { default as UNISWAP_V2_ROUTER, UNISWAP_V2_ROUTER_CONTRACT } from './uniswap-v2.js';
Expand All @@ -14,7 +21,7 @@ import { Transaction } from '../index.js';
export { ERC20, ERC721, WETH };
export { UNISWAP_V2_ROUTER_CONTRACT, UNISWAP_V3_ROUTER_CONTRACT, KYBER_NETWORK_PROXY_CONTRACT };

export { Decoder, createContract, events };
export { Decoder, createContract, events, deployContract };
// Export decoder related types
export type { ContractInfo, ContractABI };

Expand Down
18 changes: 12 additions & 6 deletions src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import { astr, add0x, ethHex, strip0x } from './utils.js';

export const addr = {
RE: /^(0[xX])?([0-9a-fA-F]{40})?$/,
parse(address: string) {
parse(address: string, allowEmpty = false) {
astr(address);
// NOTE: empty address allowed for 'to', but would be mistake for other address fields.
// '0x' instead of null/undefined because we don't want to send contract creation tx if user
// accidentally missed 'to' field.
if (allowEmpty && address === '0x') return { hasPrefix: true, data: '' };
const res = address.match(addr.RE) || [];
const hasPrefix = res[1] != null;
const data = res[2];
Expand All @@ -22,10 +26,11 @@ export const addr = {
* Address checksum is calculated by hashing with keccak_256.
* It hashes *string*, not a bytearray: keccak('beef') not keccak([0xbe, 0xef])
* @param nonChecksummedAddress
* @param allowEmpty - allows '0x'
* @returns checksummed address
*/
addChecksum(nonChecksummedAddress: string): string {
const low = addr.parse(nonChecksummedAddress).data.toLowerCase();
addChecksum(nonChecksummedAddress: string, allowEmpty = false): string {
const low = addr.parse(nonChecksummedAddress, allowEmpty).data.toLowerCase();
const hash = bytesToHex(keccak_256(low));
let checksummed = '';
for (let i = 0; i < low.length; i++) {
Expand Down Expand Up @@ -66,11 +71,12 @@ export const addr = {
/**
* Verifies checksum if the address is checksummed.
* Always returns true when the address is not checksummed.
* @param allowEmpty - allows '0x'
*/
isValid(checksummedAddress: string): boolean {
isValid(checksummedAddress: string, allowEmpty = false): boolean {
let parsed: { hasPrefix: boolean; data: string };
try {
parsed = addr.parse(checksummedAddress);
parsed = addr.parse(checksummedAddress, allowEmpty);
} catch (error) {
return false;
}
Expand All @@ -79,6 +85,6 @@ export const addr = {
const low = address.toLowerCase();
const upp = address.toUpperCase();
if (address === low || address === upp) return true;
return addr.addChecksum(low) === checksummedAddress;
return addr.addChecksum(low, allowEmpty) === checksummedAddress;
},
};
2 changes: 1 addition & 1 deletion src/net/uniswap-v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function txData(
| 'exactOutputSingle';
// TODO: remove unknown
const calldatas = [(ROUTER_CONTRACT[method].encodeInput as (v: unknown) => Uint8Array)(args)];
if (input === 'eth' && amountOut) calldatas.push(ROUTER_CONTRACT['refundETH'].encodeInput({}));
if (input === 'eth' && amountOut) calldatas.push(ROUTER_CONTRACT['refundETH'].encodeInput());
// unwrap
if (routerMustCustody) {
calldatas.push(
Expand Down
Loading

0 comments on commit 731fb38

Please sign in to comment.