Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for other account types. #252

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
11 changes: 3 additions & 8 deletions packages/client/src/Token.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -95,18 +95,15 @@ export function validateToken(api: LogionNodeApiClass, itemToken: ItemTokenWithR
error: "token ID's 'id' field is not a string",
};
}

return { valid: true };
} else {
return result;
}
} 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 {
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/node-api/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
70 changes: 49 additions & 21 deletions packages/node-api/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

Expand Down
75 changes: 71 additions & 4 deletions packages/node-api/test/Types.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
});
})

1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading