import { describe, expect, it } from 'vitest'; import { InvalidTxError } from 'polkadot-api'; import { amountText, geckoLink } from '../../src/lib/wallet/gecko'; import { failure, injectedNames, mayLetGo, testExtrinsic } from '../../src/lib/wallet/injected'; const ACCOUNT = 'g1L5EYvvSBaYuQCPW78bHxYcvHfmWAqHYe3Gsrp9SV4kWZaEM'; const COMMENT = `ld1:${'ab'.repeat(32)}`; describe('Ğecko', () => { it('the link and the QR code carry the account, the amount in Ğ1 and the sealed comment', () => { expect(geckoLink(ACCOUNT, 10, COMMENT)).toBe(`g1://${ACCOUNT}?amount=0.1&comment=${COMMENT}`); // Ğ1nkgo keeps [A-Za-z0-9 .;:!?()-_@&<>%]: the comment passes untouched. expect(/^[A-Za-z0-9 .;:!?()\-_@&<>%]+$/.test(COMMENT)).toBe(true); }); it('amounts in cents read as Ğ1', () => { expect([amountText(10), amountText(100), amountText(125), amountText(1)]).toEqual(['0.1', '1', '1.25', '0.01']); }); }); describe('wallets of the page', () => { it('every extension offering the standard interface is found, the one for Ğ1 first', () => { const win = { injectedWeb3: { 'polkadot-js': { enable: async () => ({}) }, 'duniter-connect': { enable: async () => ({}) }, broken: {} } }; expect(injectedNames(win)).toEqual(['duniter-connect', 'polkadot-js']); expect(injectedNames({})).toEqual([]); }); it('what a wallet answers is told in words', () => { expect(failure(new Error('Cancelled'))).toBe('cancelled'); expect(failure(new Error('Rejected by user'))).toBe('cancelled'); expect(failure({ type: 'Invalid', value: { type: 'Payment' } })).toBe('funds'); expect(failure(new Error('Inability to pay some fees (e.g. account balance too low)'))).toBe('funds'); expect(failure(new Error('socket hang up'))).toBe('wallet'); // Refused by the pool of the node: it will never be included. expect(failure({ type: 'Invalid', value: { type: 'Stale' } })).toBe('chain'); expect(failure({ code: 1010, message: 'Invalid Transaction' })).toBe('chain'); }); it('a refusal of the node, as polkadot-api raises it, is the chain saying no (relecture of 7ffa4fd)', () => { // The real class: its message is indented JSON (`"type": "Invalid"`, with a space). for (const reason of ['Stale', 'AncientBirthBlock', 'BadProof', 'ExhaustsResources', 'Future']) { const e = new InvalidTxError({ type: 'Invalid', value: { type: reason } }); expect(failure(e), reason).toBe('chain'); expect(mayLetGo(failure(e), true), reason).toBe(true); } expect(failure(new InvalidTxError({ type: 'Invalid', value: { type: 'Payment' } }))).toBe('funds'); expect(failure(new InvalidTxError({ type: 'Unknown', value: { type: 'CannotLookup' } }))).toBe('chain'); }); it('a ballot is let go when nothing left the page, or when the chain said no — kept when it may still land', () => { // Before the signature (the node down before sending, a refusal, the window closed): nothing left. for (const f of ['node', 'wallet', 'cancelled', 'funds', 'account', 'chain'] as const) expect(mayLetGo(f, false), f).toBe(true); // Signed and handed to the node: it may reach the chain, unless the chain refused it. for (const f of ['funds', 'account', 'chain'] as const) expect(mayLetGo(f, true), f).toBe(true); for (const f of ['node', 'wallet'] as const) expect(mayLetGo(f, true), f).toBe(false); }); it('in tests, the fake node receives what was signed, readable', () => { const hex = testExtrinsic({ address: '5Grw', dest: ACCOUNT, amount: '10', remark: COMMENT, signature: '0x01' }); expect(hex.startsWith('0x')).toBe(true); const back = JSON.parse(new TextDecoder().decode(Uint8Array.from((hex.slice(2).match(/../g) ?? []).map((b) => parseInt(b, 16))))); expect(back).toEqual({ address: '5Grw', dest: ACCOUNT, amount: '10', remark: COMMENT, signature: '0x01' }); }); });