diff --git a/packages/client/package.json b/packages/client/package.json index b7e7878b..0b9e5ebc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@logion/client", - "version": "0.43.0-5", + "version": "0.43.0-6", "description": "logion SDK for client applications", "main": "dist/index.js", "packageManager": "yarn@3.2.0", diff --git a/packages/client/src/Token.ts b/packages/client/src/Token.ts index 37efef11..25787fe4 100644 --- a/packages/client/src/Token.ts +++ b/packages/client/src/Token.ts @@ -1,4 +1,4 @@ -import { LogionNodeApiClass, AnyAccountId } from "@logion/node-api"; +import { LogionNodeApiClass, ValidAccountId } from "@logion/node-api"; import { isHex } from "@polkadot/util"; export interface ItemTokenWithRestrictedType { @@ -95,7 +95,7 @@ export function validateToken(api: LogionNodeApiClass, itemToken: ItemTokenWithR error: "token ID's 'id' field is not a string", }; } - + return { valid: true }; } else { return result; @@ -103,10 +103,7 @@ export function validateToken(api: LogionNodeApiClass, itemToken: ItemTokenWithR } else if(itemToken.type.includes("erc20")) { return validateErcToken(itemToken).result; } else if(itemToken.type === "owner") { - if ( - isHex(itemToken.id, ETHEREUM_ADDRESS_LENGTH_IN_BITS) || - AnyAccountId.isValidBech32Address(itemToken.id, "erd1") || - api.queries.isValidAccountId(itemToken.id)) { + if (ValidAccountId.fromUnknown(itemToken.id) !== undefined) { return { valid: true }; } else { return { @@ -146,8 +143,6 @@ export function isErcNft(type: TokenType): boolean { return type.includes("erc721") || type.includes("erc1155"); } -const ETHEREUM_ADDRESS_LENGTH_IN_BITS = 20 * 8; - export function validateErcToken(itemToken: ItemTokenWithRestrictedType): { result: TokenValidationResult, idObject?: any } { // eslint-disable-line @typescript-eslint/no-explicit-any let idObject; try { diff --git a/packages/node-api/package.json b/packages/node-api/package.json index a9c7dff4..941577a3 100644 --- a/packages/node-api/package.json +++ b/packages/node-api/package.json @@ -1,6 +1,6 @@ { "name": "@logion/node-api", - "version": "0.29.0-6", + "version": "0.29.0-7", "description": "logion API", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -47,6 +47,7 @@ "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", "@types/uuid": "^9.0.2", + "bech32": "^2.0.0", "fast-sha256": "^1.3.0", "uuid": "^9.0.0" }, diff --git a/packages/node-api/src/Types.ts b/packages/node-api/src/Types.ts index cea9afda..0cb0cbbc 100644 --- a/packages/node-api/src/Types.ts +++ b/packages/node-api/src/Types.ts @@ -4,7 +4,8 @@ import { isHex } from "@polkadot/util"; import { UUID } from "./UUID.js"; import { Hash } from './Hash.js'; import { Lgnt } from './Currency.js'; -import { encodeAddress, validateAddress, addressEq } from "@polkadot/util-crypto"; +import { encodeAddress, addressEq, base58Decode, checkAddressChecksum } from "@polkadot/util-crypto"; +import { bech32 } from "bech32"; export interface TypesAccountData { available: bigint, @@ -158,6 +159,7 @@ export interface TypesRecoveryConfig { } export type AccountType = "Polkadot" | "Ethereum" | "Bech32"; +const ACCOUNT_TYPES: AccountType[] = ["Polkadot", "Ethereum", "Bech32"]; export const ETHEREUM_ADDRESS_LENGTH_IN_BITS = 20 * 8; @@ -189,7 +191,7 @@ export class AnyAccountId implements AccountId { } validate(): string | undefined { - if(!["Polkadot", "Ethereum", "Bech32"].includes(this.type)) { + if(!ACCOUNT_TYPES.includes(this.type)) { return `Unsupported address type ${this.type}`; } if(this.type === "Ethereum" && !isHex(this.address, ETHEREUM_ADDRESS_LENGTH_IN_BITS)) { @@ -206,32 +208,28 @@ export class AnyAccountId implements AccountId { private validPolkadotAccountId(): string | undefined { try { - if (validateAddress(this.address, false)) { + const decoded = base58Decode(this.address); + const [isValid,,,] = checkAddressChecksum(decoded); + if (isValid) { return undefined } else { - return "Not valid" + return "Invalid decoded address" } - } catch(e) { - return String(e); + } catch(error) { + return (error as Error).message } } - static isValidBech32Address(address: string, prefix?: string): boolean { - if (prefix && !address.startsWith(prefix)) { - return false; - } - // TODO Improve by verifying the checksum - // @see https://github.com/sipa/bech32/blob/master/ref/javascript/bech32.js#L95 - for (let p = 0; p < address.length; ++p) { - if (address.charCodeAt(p) < 33 || address.charCodeAt(p) > 126) { + isValidBech32Address(prefix?: string): boolean { + try { + if (prefix && !this.address.startsWith(prefix)) { return false; } + const decoded = bech32.decode(this.address); + return prefix === undefined || decoded.prefix === prefix; + } catch (error) { + return false; } - return true; - } - - isValidBech32Address(prefix?: string): boolean { - return AnyAccountId.isValidBech32Address(this.address, prefix); } toValidAccountId(): ValidAccountId { @@ -294,7 +292,7 @@ export class ValidAccountId implements AccountId { throw new Error(error); } - this.anyAccountId = new AnyAccountId(ValidAccountId.computeAddress(SS58_PREFIX, accountId.address, accountId.type), accountId.type); + this.anyAccountId = new AnyAccountId(ValidAccountId.normalizeAddress(accountId.address, accountId.type), accountId.type); } private anyAccountId: AnyAccountId; @@ -341,11 +339,41 @@ export class ValidAccountId implements AccountId { return new AnyAccountId(address, "Polkadot").toValidAccountId(); } + static ethereum(address: string): ValidAccountId { + return new AnyAccountId(address, "Ethereum").toValidAccountId(); + } + + static bech32(address: string): ValidAccountId { + return new AnyAccountId(address, "Bech32").toValidAccountId(); + } + + /** + * Attempt to guess the account type from a given address, and instantiate + * the corresponding valid account. + * Warning: this method should NOT be used whenever caller site knows the account type. + * In this case use {@link polkadot}, {@link ethereum}, {@link bech32} or {@link ValidAccountId:constructor} + * + * @param address the address. + * @returns a valid account or undefined. + */ + static fromUnknown(address: string): ValidAccountId | undefined { + for (const type of ACCOUNT_TYPES) { + const account = new AnyAccountId(address, type); + if (account.isValid()) { + return account.toValidAccountId(); + } + } + } + + private static normalizeAddress(address: string, type: AccountType): string { + return this.computeAddress(SS58_PREFIX, address, type) + } + private static computeAddress(prefix: number, address: string, type: AccountType): string { if (type === 'Polkadot') { return encodeAddress(address, prefix); } - return address + return address.toLowerCase(); } } diff --git a/packages/node-api/test/Types.spec.ts b/packages/node-api/test/Types.spec.ts index d35082d1..0f93ece7 100644 --- a/packages/node-api/test/Types.spec.ts +++ b/packages/node-api/test/Types.spec.ts @@ -1,6 +1,58 @@ import { ValidAccountId, AnyAccountId } from "../src/index.js"; -describe("ValidAccountId", () => { +describe("ValidAccountId (Bech32)", () => { + + const address1 = "erd1sqcp77ll8v8j6vgs5m0h2xxkz5wv26pfj2vyyls52cz6hdumkmjq0jr93a"; + const address2 = "ERD1SQCP77LL8V8J6VGS5M0H2XXKZ5WV26PFJ2VYYLS52CZ6HDUMKMJQ0JR93A"; + + it("is valid and not case-sensitive", () => { + const account1 = ValidAccountId.bech32(address1); + const account2 = ValidAccountId.bech32(address2); + + expect(account1.equals(account1)).toBeTrue(); + expect(account1.equals(account2)).toBeTrue(); + + expect(account2.equals(account1)).toBeTrue(); + expect(account2.equals(account2)).toBeTrue(); + }) + + it("guesses from unknown", () => { + expect(ValidAccountId.fromUnknown(address1)?.type).toEqual("Bech32"); + expect(ValidAccountId.fromUnknown(address2)?.type).toEqual("Bech32"); + }) +}) + +describe("ValidAccountId (Ethereum)", () => { + + const address1 = "0x6ef154673a6379b2CDEDeD6aF1c0d705c3c8272a"; + const address2 = "0x6ef154673a6379b2cdeded6af1c0d705c3c8272a"; + const address3 = "0x6EF154673A6379B2CDEDED6AF1C0D705C3C8272A"; + + it("is valid and not case-sensitive", () => { + const account1 = ValidAccountId.ethereum(address1); + const account2 = ValidAccountId.ethereum(address2); + const account3 = ValidAccountId.ethereum(address3); + + expect(account1.equals(account1)).toBeTrue(); + expect(account1.equals(account2)).toBeTrue(); + expect(account1.equals(account3)).toBeTrue(); + + expect(account2.equals(account1)).toBeTrue(); + expect(account2.equals(account2)).toBeTrue(); + expect(account2.equals(account3)).toBeTrue(); + + expect(account3.equals(account1)).toBeTrue(); + expect(account3.equals(account2)).toBeTrue(); + expect(account3.equals(account3)).toBeTrue(); + }) + + it("guesses from unknown", () => { + expect(ValidAccountId.fromUnknown(address1)?.type).toEqual("Ethereum"); + expect(ValidAccountId.fromUnknown(address2)?.type).toEqual("Ethereum"); + }) +}) + +describe("ValidAccountId (Polkadot)", () => { const address42 = "5HYf6QFkYpso8FdX9WALCmRTcga7YSmuFS5qqaJtFF7m4RPr"; const address2021 = "vQxmTQGRHbTsBdDhVLqsksX7c44K8DjVokJUi8ZK58z88tDBx"; @@ -39,11 +91,26 @@ describe("ValidAccountId", () => { it("does not validate an invalid account", () => { expect(new AnyAccountId("BLA", "Polkadot").validate()) - .toEqual("Wrong Polkadot address BLA: Error: Decoding BLA: Invalid decoded address length") + .toEqual("Wrong Polkadot address BLA: Invalid decoded address") expect(new AnyAccountId("INVALID", "Polkadot").validate()) - .toEqual('Wrong Polkadot address INVALID: Error: Decoding INVALID: Invalid base58 character "I" (0x49) at index 0') + .toEqual('Wrong Polkadot address INVALID: Invalid base58 character "I" (0x49) at index 0') const invalid = "5HMzQmyDb8CU8ajJuvSrrqSH5LPHNRFS8888888888888888"; expect(new AnyAccountId(invalid, "Polkadot").validate()) - .toEqual("Wrong Polkadot address 5HMzQmyDb8CU8ajJuvSrrqSH5LPHNRFS8888888888888888: Error: Decoding 5HMzQmyDb8CU8ajJuvSrrqSH5LPHNRFS8888888888888888: Invalid decoded address checksum") + .toEqual("Wrong Polkadot address 5HMzQmyDb8CU8ajJuvSrrqSH5LPHNRFS8888888888888888: Invalid decoded address") + }) + + it("guesses from unknown", () => { + expect(ValidAccountId.fromUnknown(address42)?.type).toEqual("Polkadot"); + expect(ValidAccountId.fromUnknown(address2021)?.type).toEqual("Polkadot"); }) }) + +describe("ValidAccountId: fromUnknown()", () => { + + it("returns undefined", () => { + expect(ValidAccountId.fromUnknown("BLA")).toBeUndefined(); + expect(ValidAccountId.fromUnknown("INVALID")).toBeUndefined(); + expect(ValidAccountId.fromUnknown("0x0000")).toBeUndefined(); + }); +}) + diff --git a/yarn.lock b/yarn.lock index 8746afbf..8999bfc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,6 +3077,7 @@ __metadata: "@types/uuid": ^9.0.2 "@typescript-eslint/eslint-plugin": ^6.9.1 "@typescript-eslint/parser": ^6.9.1 + bech32: ^2.0.0 eslint: ^8.20.0 fast-sha256: ^1.3.0 jasmine: ^4.3.0