Skip to content

Commit

Permalink
Merge pull request #252 from logion-network/feature/manage-other-acco…
Browse files Browse the repository at this point in the history
…unts

Add support for other account types.
  • Loading branch information
benoitdevos authored Apr 17, 2024
2 parents 8ce551b + 8757143 commit 60e93bd
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 35 deletions.
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

0 comments on commit 60e93bd

Please sign in to comment.