Skip to content

Commit 2158bc0

Browse files
authored
fix: improve input DevEx for ink!v6 (#590)
1 parent c66760f commit 2158bc0

File tree

11 files changed

+112
-78
lines changed

11 files changed

+112
-78
lines changed

cypress/e2e/contracts/erc20.spec.ts

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ describe('ERC20 Contract ', () => {
2323
});
2424

2525
it('contract file uploads', () => {
26-
// TODO: In the contract, replaced Address with H160. Fix https://github.com/use-ink/contracts-ui/issues/582
2726
assertUpload('erc20.contract');
2827
});
2928

@@ -54,67 +53,36 @@ describe('ERC20 Contract ', () => {
5453

5554
it(`transfers ${transferValue} Units to another account`, () => {
5655
selectMessage('transfer', 3);
57-
cy.get('.form-field.to')
58-
.find("input[type='text']")
59-
.clear()
60-
.type('0x60afa252b554aabc4b3253ca2be60dc1d536ec10')
61-
.should('have.value', '0x60afa252b554aabc4b3253ca2be60dc1d536ec10');
62-
cy.get('.form-field.value').find('input[type="number"]').type(`${transferValue}`);
56+
cy.get('.form-field.to').find('.dropdown').click().find('.dropdown__option').eq(3).click();
57+
cy.get('.form-field.value').find('input[type="number"]').eq(0).type(`${transferValue}`);
6358
assertCall();
6459
selectMessage('balanceOf', 1);
60+
console.log(initialSupply - transferValue);
61+
cy.get('.form-field.owner').find('.dropdown').click().find('.dropdown__option').eq(0).click();
6562

66-
cy.get('.form-field.owner')
67-
.find("input[type='text']")
68-
.clear()
69-
.type('0x9621dde636de098b43efb0fa9b61facfe328f99d')
70-
.should('have.value', '0x9621dde636de098b43efb0fa9b61facfe328f99d');
7163
assertReturnValue('balanceOf', `${initialSupply - transferValue}`);
7264
});
7365

7466
it(`successfully approves allowance`, () => {
7567
selectMessage('approve', 4);
76-
cy.get('.form-field.spender')
77-
.find("input[type='text']")
78-
.clear()
79-
.type('0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01')
80-
.should('have.value', '0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01');
81-
cy.get('.form-field.value').find('input[type="number"]').type(`${allowance}`);
68+
cy.get('.form-field.spender').find('.dropdown').click().find('.dropdown__option').eq(2).click();
69+
cy.get('.form-field.value').find('input[type="number"]').eq(0).type(`${allowance}`);
8270
assertCall();
8371
selectMessage('allowance', 2);
84-
cy.get('.form-field.owner')
85-
.find("input[type='text']")
86-
.clear()
87-
.type('0x9621dde636de098b43efb0fa9b61facfe328f99d')
88-
.should('have.value', '0x9621dde636de098b43efb0fa9b61facfe328f99d');
89-
cy.get('.form-field.spender')
90-
.find("input[type='text']")
91-
.clear()
92-
.type('0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01')
93-
.should('have.value', '0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01');
72+
cy.get('.form-field.owner').find('.dropdown').click().find('.dropdown__option').eq(0).click();
73+
cy.get('.form-field.spender').find('.dropdown').click().find('.dropdown__option').eq(2).click();
9474
assertReturnValue('allowance', `${allowance}`);
9575
});
9676

9777
it(`transfers ${transferValue} on behalf of alice`, () => {
9878
cy.get('.form-field.caller').click().find('.dropdown__option').eq(2).click();
9979
selectMessage('transferFrom', 5);
100-
cy.get('.form-field.from')
101-
.find("input[type='text']")
102-
.clear()
103-
.type('0x9621dde636de098b43efb0fa9b61facfe328f99d')
104-
.should('have.value', '0x9621dde636de098b43efb0fa9b61facfe328f99d');
105-
cy.get('.form-field.to')
106-
.find("input[type='text']")
107-
.clear()
108-
.type('0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01')
109-
.should('have.value', '0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01');
110-
cy.get('.form-field.value').find('input[type="number"]').type(`${transferValue}`);
80+
cy.get('.form-field.from').find('.dropdown').click().find('.dropdown__option').eq(0).click();
81+
cy.get('.form-field.to').find('.dropdown').click().find('.dropdown__option').eq(2).click();
82+
cy.get('.form-field.value').find('input[type="number"]').eq(0).type(`${transferValue}`);
11183
assertCall();
11284
selectMessage('balanceOf', 1);
113-
cy.get('.form-field.owner')
114-
.find("input[type='text']")
115-
.clear()
116-
.type('0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01')
117-
.should('have.value', '0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01');
85+
cy.get('.form-field.owner').find('.dropdown').click().find('.dropdown__option').eq(2).click();
11886
assertReturnValue('balanceOf', `${transferValue}`);
11987
});
12088
});

cypress/e2e/contracts/mother.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
assertMoveToStep3,
99
assertContractRedirect,
1010
assertInstantiate,
11+
selectAccount,
1112
} from '../../support/util';
1213

1314
describe('Mother Contract ', () => {
@@ -76,12 +77,9 @@ describe('Mother Contract ', () => {
7677
.within(() => {
7778
cy.get('[data-cy="switch-button"]').click();
7879
cy.contains('0: H160').should('be.visible');
79-
cy.get("input[type='text']")
80-
.clear()
81-
.type('0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01')
82-
.should('have.value', '0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01');
8380
cy.contains('1: u128').should('be.visible');
8481
cy.get("input[type='number']").should('have.lengthOf', 1).type('99999');
82+
selectAccount('bob', 2);
8583
});
8684
});
8785
});

cypress/fixtures/erc20.contract

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/lib/callOptions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function transformUserInput(
5757
messageArgs: AbiParam[],
5858
values?: Record<string, unknown>,
5959
): unknown[] {
60-
return messageArgs.map(({ name, type: { type } }) => {
60+
return messageArgs.map(({ name, type: { type, displayName } }) => {
6161
const value = values ? values[name] : null;
6262

6363
if (type === 'Balance') {
@@ -67,6 +67,11 @@ export function transformUserInput(
6767
return registry.createType('U256', value);
6868
}
6969

70+
// H160 and Address types need explicit type creation
71+
if (type === 'H160' || type === 'Address' || displayName?.includes('Address')) {
72+
return registry.createType('H160', value);
73+
}
74+
7075
return value;
7176
});
7277
}

src/lib/initValue.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,52 @@ import type { Registry, TypeDef } from '@polkadot/types/types';
66

77
import { getTypeDef } from '@polkadot/types';
88
import { TypeDefInfo } from '@polkadot/types/types';
9+
import { decodeAddress } from '@polkadot/keyring';
910
import { BN_ZERO, isBn } from './bn';
1011
import { Account } from 'types';
12+
import { toEthAddress } from './address';
13+
import type { InkVersion } from 'ui/contexts/VersionContext';
1114

1215
const warnList: string[] = [];
1316

14-
export function getInitValue(registry: Registry, accounts: Account[], def: TypeDef): unknown {
17+
export function getInitValue(
18+
registry: Registry,
19+
accounts: Account[],
20+
def: TypeDef,
21+
version?: InkVersion,
22+
): unknown {
1523
if (def.info === TypeDefInfo.Si) {
1624
const lookupTypeDef = registry.lookup.getTypeDef(def.lookupIndex as number);
1725

18-
return getInitValue(registry, accounts, lookupTypeDef);
26+
return getInitValue(registry, accounts, lookupTypeDef, version);
1927
} else if (def.info === TypeDefInfo.Option) {
2028
return null;
2129
} else if (def.info === TypeDefInfo.Vec) {
22-
return [getInitValue(registry, accounts, def.sub as TypeDef)];
30+
return [getInitValue(registry, accounts, def.sub as TypeDef, version)];
2331
} else if (def.info === TypeDefInfo.VecFixed) {
2432
const value = [];
2533
const length = def.length as number;
2634

2735
for (let i = 0; i < length; i++) {
28-
value.push(getInitValue(registry, accounts, def.sub as TypeDef));
36+
value.push(getInitValue(registry, accounts, def.sub as TypeDef, version));
2937
}
3038

3139
return value;
3240
} else if (def.info === TypeDefInfo.Tuple) {
33-
return Array.isArray(def.sub) ? def.sub.map(def => getInitValue(registry, accounts, def)) : [];
41+
return Array.isArray(def.sub)
42+
? def.sub.map(def => getInitValue(registry, accounts, def, version))
43+
: [];
3444
} else if (def.info === TypeDefInfo.Struct) {
3545
return Array.isArray(def.sub)
3646
? def.sub.reduce((result: Record<string, unknown>, def): Record<string, unknown> => {
37-
result[def.name as string] = getInitValue(registry, accounts, def);
47+
result[def.name as string] = getInitValue(registry, accounts, def, version);
3848

3949
return result;
4050
}, {})
4151
: {};
4252
} else if (def.info === TypeDefInfo.Enum) {
4353
return Array.isArray(def.sub)
44-
? { [def.sub[0].name as string]: getInitValue(registry, accounts, def.sub[0]) }
54+
? { [def.sub[0].name as string]: getInitValue(registry, accounts, def.sub[0], version) }
4555
: {};
4656
}
4757

@@ -117,8 +127,16 @@ export function getInitValue(registry: Registry, accounts: Account[], def: TypeD
117127
return '';
118128
}
119129

120-
case 'AccountIdOf':
121130
case 'Address':
131+
try {
132+
const address = accounts[0].address;
133+
// ink v6 uses H160 (Ethereum-style) addresses
134+
return version === 'v6' ? toEthAddress(decodeAddress(address)) : address;
135+
} catch (e) {
136+
return '';
137+
}
138+
139+
case 'AccountIdOf':
122140
case 'Call':
123141
case 'CandidateReceipt':
124142
case 'Digest':
@@ -151,7 +169,7 @@ export function getInitValue(registry: Registry, accounts: Account[], def: TypeD
151169
} else if ([TypeDefInfo.Struct].includes(raw.info)) {
152170
return undefined;
153171
} else if ([TypeDefInfo.Enum, TypeDefInfo.Tuple].includes(raw.info)) {
154-
return getInitValue(registry, accounts, raw);
172+
return getInitValue(registry, accounts, raw, version);
155173
}
156174
} catch (e) {
157175
error = (e as Error).message;

src/ui/components/account/Select.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Dropdown } from '../common/Dropdown';
77
import { Account } from './Account';
88
import { createAccountOptions } from 'ui/util/dropdown';
99
import type { DropdownOption, DropdownProps, ValidFormField } from 'types';
10-
import { useApi, useDatabase } from 'ui/contexts';
10+
import { useApi, useDatabase, useVersion } from 'ui/contexts';
1111
import { classes } from 'lib/util';
1212
import { useDbQuery } from 'ui/hooks';
1313

@@ -52,6 +52,7 @@ export function AccountSelect({ placeholder = 'Select account', ...props }: Prop
5252
export function AddressSelect({ placeholder = 'Select account', onChange, ...props }: Props) {
5353
const { accounts } = useApi();
5454
const { db } = useDatabase();
55+
const { version } = useVersion();
5556
const [contracts] = useDbQuery(() => db.contracts.toArray(), [db]);
5657
const [recent, setRecent] = useState<DropdownOption<string>[]>([]);
5758

@@ -63,7 +64,7 @@ export function AddressSelect({ placeholder = 'Select account', onChange, ...pro
6364
},
6465
{
6566
label: 'My Accounts',
66-
options: createAccountOptions(accounts || []),
67+
options: createAccountOptions(accounts || [], version),
6768
},
6869
{
6970
label: 'Uploaded Contracts',
@@ -73,7 +74,7 @@ export function AddressSelect({ placeholder = 'Select account', onChange, ...pro
7374
})),
7475
},
7576
];
76-
}, [accounts, contracts, recent]);
77+
}, [accounts, contracts, recent, version]);
7778

7879
const handleCreate = (inputValue: string) => {
7980
setRecent([...recent, { label: inputValue, value: inputValue }]);

src/ui/components/form/InputBn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function getMinMax(type: string): [bigint, bigint] {
3636
}
3737

3838
export function InputBn({ onChange, typeDef: { type } }: Props): React.ReactElement {
39-
const [displayValue, setDisplayValue] = useState('0');
39+
const [displayValue, setDisplayValue] = useState('');
4040
const [min, max] = getMinMax(type);
4141

4242
const handleChange = useCallback(

src/ui/components/form/findComponent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export function findComponent(
131131
switch (type.type) {
132132
case 'AccountId':
133133
case 'Address':
134+
case 'H160':
134135
return AddressSelect;
135136

136137
case 'Balance':

src/ui/hooks/useArgValues.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22
// SPDX-License-Identifier: GPL-3.0-only
33

44
import { useEffect, useMemo, useRef, useState } from 'react';
5-
import { useApi } from 'ui/contexts/ApiContext';
5+
import { useApi, useVersion } from 'ui/contexts';
66
import { AbiMessage, AbiParam, Account, Registry, SetState } from 'types';
77
import { getInitValue } from 'lib/initValue';
88
import { transformUserInput } from 'lib/callOptions';
9+
import type { InkVersion } from 'ui/contexts/VersionContext';
910

1011
type ArgValues = Record<string, unknown>;
1112

12-
function fromArgs(registry: Registry, accounts: Account[], args: AbiParam[]): ArgValues {
13+
function fromArgs(
14+
registry: Registry,
15+
accounts: Account[],
16+
args: AbiParam[],
17+
version?: InkVersion,
18+
): ArgValues {
1319
const result: ArgValues = {};
1420

1521
args?.forEach(({ name, type }) => {
16-
result[name] = getInitValue(registry, accounts, type);
22+
result[name] = getInitValue(registry, accounts, type, version);
1723
});
1824

1925
return result;
@@ -24,27 +30,40 @@ export function useArgValues(
2430
registry: Registry,
2531
): [ArgValues, SetState<ArgValues>, Uint8Array | undefined] {
2632
const { accounts } = useApi();
33+
const { version } = useVersion();
2734
const [value, setValue] = useState<ArgValues>(
28-
accounts && message ? fromArgs(registry, accounts, message.args) : {},
35+
accounts && message ? fromArgs(registry, accounts, message.args, version) : {},
2936
);
3037
const argsRef = useRef(message?.args ?? []);
3138

3239
const inputData = useMemo(() => {
3340
let data: Uint8Array | undefined;
3441
try {
35-
data = message?.toU8a(transformUserInput(registry, message.args, value));
42+
if (version === 'v6' && message) {
43+
const patchedArgs = message.args.map(arg => {
44+
if (arg.type.type === 'Address') {
45+
return { ...arg, type: { ...arg.type, type: 'H160' } };
46+
}
47+
return arg;
48+
});
49+
50+
const patchedMessage = { ...message, args: patchedArgs };
51+
data = patchedMessage.toU8a(transformUserInput(registry, patchedArgs, value));
52+
} else {
53+
data = message?.toU8a(transformUserInput(registry, message.args, value));
54+
}
3655
} catch (e) {
3756
console.error(e);
3857
}
3958
return data;
40-
}, [value, registry, message]);
59+
}, [value, registry, message, version]);
4160

4261
useEffect((): void => {
4362
if (accounts && message && argsRef.current !== message.args) {
44-
setValue(fromArgs(registry, accounts, message.args));
63+
setValue(fromArgs(registry, accounts, message.args, version));
4564
argsRef.current = message.args;
4665
}
47-
}, [accounts, message, registry]);
66+
}, [accounts, message, registry, version]);
4867

4968
return [value, setValue, inputData];
5069
}

src/ui/hooks/useStoredContract.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import { useLiveQuery } from 'dexie-react-hooks';
55
import { useNavigate } from 'react-router-dom';
66
import { useState } from 'react';
7-
import { useApi, useDatabase } from 'ui/contexts';
7+
import { useApi, useDatabase, useVersion } from 'ui/contexts';
88
import { ContractDocument, ContractPromise, UIContract } from 'types';
99

1010
export function useStoredContract(address: string): UIContract | undefined {
1111
const navigate = useNavigate();
1212
const { api } = useApi();
1313
const { db } = useDatabase();
14+
const { version } = useVersion();
1415
const [contract, setContract] = useState<ContractPromise>();
1516
const [document, setDocument] = useState<ContractDocument>();
1617

@@ -24,10 +25,23 @@ export function useStoredContract(address: string): UIContract | undefined {
2425
navigate('/');
2526
} else {
2627
const c = new ContractPromise(api, d.abi, address);
28+
29+
// TODO: Temporary workaround: ink v6 uses Address as a type alias for H160, but polkadot.js
30+
// encodes them differently. Patch the ABI to use H160 for consistent encoding.
31+
if (version === 'v6') {
32+
c.abi.messages.forEach(message => {
33+
message.args.forEach(arg => {
34+
if (arg.type.type === 'Address') {
35+
arg.type.type = 'H160';
36+
}
37+
});
38+
});
39+
}
40+
2741
setDocument(d);
2842
setContract(c);
2943
}
30-
}, [address]);
44+
}, [address, version]);
3145

3246
if (!document || !contract) return undefined;
3347

0 commit comments

Comments
 (0)