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

Support for MultiversX addresses, tokens and signatures. #170

Merged
merged 1 commit into from
Jul 4, 2023
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.28.0",
"version": "0.28.1-3",
"description": "logion SDK for client applications",
"main": "dist/index.js",
"packageManager": "yarn@3.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/Signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface SignRawParameters {
attributes: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any
}

export type SignatureType = "POLKADOT" | "ETHEREUM" | "CROSSMINT_ETHEREUM";
export type SignatureType = "POLKADOT" | "ETHEREUM" | "CROSSMINT_ETHEREUM" | "MULTIVERSX";

export interface TypedSignature {
signature: string
Expand Down
41 changes: 33 additions & 8 deletions packages/client/src/Token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LogionNodeApiClass } from "@logion/node-api";
import { LogionNodeApiClass, AnyAccountId } from "@logion/node-api";
import { isHex } from "@polkadot/util";

export interface ItemTokenWithRestrictedType {
Expand All @@ -22,9 +22,12 @@ export type TokenType =
| 'polygon_erc20'
| 'polygon_mumbai_erc20'
| 'owner'
| 'multiversx_devnet_esdt'
| 'multiversx_testnet_esdt'
| 'multiversx_esdt'
;

export type NetworkType = 'ETHEREUM' | 'POLKADOT';
export type NetworkType = 'ETHEREUM' | 'POLKADOT' | 'MULTIVERSX';

export function isTokenType(type: string): type is TokenType {
return (
Expand All @@ -42,18 +45,24 @@ export function isTokenType(type: string): type is TokenType {
|| type === 'polygon_erc20'
|| type === 'polygon_mumbai_erc20'
|| type === 'owner'
|| type === 'multiversx_devnet_esdt'
|| type === 'multiversx_testnet_esdt'
|| type === 'multiversx_esdt'
);
}

export function isTokenCompatibleWith(type: TokenType, networkType: NetworkType): boolean {
if (type === 'owner') {
return true;
}
if (networkType === 'ETHEREUM') {
return type.startsWith("ethereum")
|| type.startsWith("goerli")
|| type.startsWith("polygon")
|| type === "owner"
} else if (networkType === 'MULTIVERSX') {
return type.startsWith("multiversx")
} else {
return type === "singular_kusama"
|| type === "owner"
}
}

Expand Down Expand Up @@ -87,27 +96,39 @@ 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) || api.queries.isValidAccountId(itemToken.id)) {
if (
isHex(itemToken.id, ETHEREUM_ADDRESS_LENGTH_IN_BITS) ||
AnyAccountId.isValidBech32Address(itemToken.id, "erd1") ||
api.queries.isValidAccountId(itemToken.id)) {
return { valid: true };
} else {
return {
valid: false,
error: "token ID must be a valid Ethereum or Polkadot address",
error: "token ID must be a valid Ethereum, Polkadot or MultiversX address",
}
}
} else if(itemToken.type === "singular_kusama") {
if(isSingularKusamaId(itemToken.id)) {
if (isSingularKusamaId(itemToken.id)) {
return { valid: true };
} else {
return {
valid: false,
error: "token ID must be a valid Singular Kusama ID",
}
}
} else if (itemToken.type.startsWith("multiversx")) {
if (isMultiversxESDTId(itemToken.id)) {
return { valid: true };
} else {
return {
valid: false,
error: "token ID must be a valid MultiversX ESDT ID",
}
}
} else {
return {
valid: false,
error: `unsupported token type '${itemToken.type}'`,
error: `unsupported token type '${ itemToken.type }'`,
}
}
}
Expand Down Expand Up @@ -158,3 +179,7 @@ export function validateErcToken(itemToken: ItemTokenWithRestrictedType): { resu
export function isSingularKusamaId(tokenId: string): boolean {
return /^[0-9a-zA-Z\-_]+$/.test(tokenId);
}

export function isMultiversxESDTId(tokenId: string): boolean {
return /^[0-9A-Z]+-[0-9a-f]{6}(-[0-9a-f]+)?$/.test(tokenId);
}
54 changes: 44 additions & 10 deletions packages/client/test/Token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ const otherTypes: TokenType[] = [
"singular_kusama"
];

const esdtTypes: TokenType[] = [
"multiversx_devnet_esdt",
"multiversx_testnet_esdt",
"multiversx_esdt",
]

const allTypes: TokenType[] = [
...ercNonFungibleTypes,
...ercFungibleTypes,
...esdtTypes,
...otherTypes,
];

Expand All @@ -51,7 +58,7 @@ describe("Token", () => {
for (const type of ercNonFungibleTypes) {
it(`validates valid ${ type } token`, () => testNonFungibleErcValidToken(type));
it(`invalidates ${ type } token with non-JSON id`, () => testErcInvalidIdType(type));
it(`invalidates ${ type } token with missing id field`, () => testErcInvalidIdMisingId(type));
it(`invalidates ${ type } token with missing id field`, () => testErcInvalidIdMissingId(type));
it(`invalidates ${ type } token with missing contract field`, () => testErcInvalidIdMissingContract(type));
it(`invalidates ${ type } token with wrongly typed contract field`, () => testErcInvalidIdContractIsNotString(type));
it(`invalidates ${ type } token with wrongly typed id field`, () => testErcInvalidIdIdIsNotString(type));
Expand All @@ -68,14 +75,16 @@ describe("Token", () => {
it(`checks that ${ type } is NOT POLKADOT-compatible`, () => testIsTokenNotCompatibleWith(type, 'POLKADOT'))
}

it(`checks that owner is both POLKADOT- and ETHEREUM-compatible`, () => {
it(`checks that owner is compatible with all network type`, () => {
testIsTokenCompatibleWith('owner', 'POLKADOT');
testIsTokenCompatibleWith('owner', 'ETHEREUM');
testIsTokenCompatibleWith('owner', 'MULTIVERSX');
});

it(`checks that singular_kusama is NOT ETHEREUM-compatible`, () =>
testIsTokenNotCompatibleWith('singular_kusama', 'ETHEREUM')
);
it(`checks that singular_kusama is NOT ETHEREUM- and MULTIVERSX-compatible`, () => {
testIsTokenNotCompatibleWith('singular_kusama', 'ETHEREUM');
testIsTokenNotCompatibleWith('singular_kusama', 'MULTIVERSX');
});

it(`checks that singular_kusama is POLKADOT-compatible`, () =>
testIsTokenCompatibleWith('singular_kusama', 'POLKADOT')
Expand All @@ -94,23 +103,31 @@ describe("Token", () => {
type: "owner",
id: "5FniDvPw22DMW1TLee9N8zBjzwKXaKB2DcvZZCQU5tjmv1kb",
issuance: 1n,
}, "5FniDvPw22DMW1TLee9N8zBjzwKXaKB2DcvZZCQU5tjmv1kb");
});
});

it("validates valid owner token with MultiversX address", () => {
testValid({
type: "owner",
id: "erd1gram9hstfmfze9hcxeume8tspsed2gful2cr4ra6fefn7hl7lfeqm9ka0x",
issuance: 1n,
});
});

it("invalidates owner token with non-hex ID", () => {
testInvalid({
type: "owner",
id: 'some random string',
issuance: 1n,
}, "token ID must be a valid Ethereum or Polkadot address");
}, "token ID must be a valid Ethereum, Polkadot or MultiversX address");
});

it("invalidates owner token with hex ID but wrong length", () => {
testInvalid({
type: "owner",
id: '0xa6db31d1aee06a3ad7e4e56de3775e80d2f5ea8',
issuance: 1n,
}, "token ID must be a valid Ethereum or Polkadot address");
}, "token ID must be a valid Ethereum, Polkadot or MultiversX address");
});

it("validates valid singular_kusama token", () => {
Expand All @@ -128,6 +145,18 @@ describe("Token", () => {
issuance: 1n,
}, "token ID must be a valid Singular Kusama ID");
});

it("validates valid Multiversx tokens", () => {
const validESDTIds = [ VALID_MULTIVERSX_FT_ID, VALID_MULTIVERSX_SFT_ID, VALID_MULTIVERSX_NFT_ID ];
validESDTIds.forEach(validESDTId =>
testValid({
type: "multiversx_esdt",
issuance: 1n,
id: validESDTId,
})
)
});

});

describe("isTokenType", () => {
Expand All @@ -147,8 +176,9 @@ function testNonFungibleErcValidToken(type: TokenType) {
});
}

function testValid(token: ItemTokenWithRestrictedType, polkadotAddress?: string) {
function testValid(token: ItemTokenWithRestrictedType) {
const api = buildLogionNodeApiMock();
api.setup(instance => instance.queries.isValidAccountId).returns(() => token.id.startsWith("5"));
const result = validateToken(api.object(), token);
expect(result.valid).toBe(true);
expect(result.error).not.toBeDefined();
Expand All @@ -170,7 +200,7 @@ function testInvalid(token: ItemTokenWithRestrictedType, message: string) {
expect(result.error).toBe(message);
}

function testErcInvalidIdMisingId(type: TokenType) {
function testErcInvalidIdMissingId(type: TokenType) {
testInvalid({
type,
id: '{"contract":"0x765df6da33c1ec1f83be42db171d7ee334a46df5"}',
Expand Down Expand Up @@ -206,6 +236,10 @@ const VALID_SINGULAR_KUSAMA_TOKEN_ID = "15057162-acba02847598b67746-DSTEST1-LUXE

const INVALID_SINGULAR_KUSAMA_TOKEN_ID = "*15057162-acba02847598b67746-DSTEST1-LUXEMBOURG_HOUSE-00000001";

const VALID_MULTIVERSX_FT_ID = "LR001-5c0eb8";
const VALID_MULTIVERSX_SFT_ID = "LRCOLL001-e42371-01";
const VALID_MULTIVERSX_NFT_ID = "LNFT001-1bc5a0-01";

function testFungibleErcValidToken(type: TokenType) {
testValid({
type,
Expand Down
2 changes: 1 addition & 1 deletion packages/crossmint/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logion/crossmint",
"version": "0.1.18",
"version": "0.1.19-1",
"description": "logion SDK for Crossmint",
"main": "dist/index.js",
"packageManager": "yarn@3.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logion/extension",
"version": "0.5.16",
"version": "0.5.17-1",
"description": "logion SDK for Polkadot JS extension",
"main": "dist/index.js",
"packageManager": "yarn@3.2.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/multiversx/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/test/**
/integration/**
/src/**
/scripts/**
docker-compose.yml
front_config.js
front_web*.conf
jasmine*.json
tsconfig*.json
15 changes: 15 additions & 0 deletions packages/multiversx/jasmine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"spec_dir": "test",
"spec_files": [
"**/*.spec.ts"
],
"helpers": [
"typescript.js"
],
"stopSpecOnExpectationFailure": false,
"reporters": [
{
"name": "jasmine-spec-reporter#SpecReporter"
}
]
}
52 changes: 52 additions & 0 deletions packages/multiversx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@logion/multiversx",
"version": "0.1.0-4",
"description": "logion SDK for MultiversX",
"main": "dist/index.js",
"packageManager": "yarn@3.2.0",
"type": "module",
"scripts": {
"build": "yarn lint && tsc",
"lint": "yarn eslint src/**",
"test": "echo 'Nothing to test for the moment'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/logion-network/logion-api.git",
"directory": "packages/multiversx"
},
"keywords": [
"logion"
],
"author": "Logion Team",
"license": "Apache-2.0",
"dependencies": {
"@logion/client": "workspace:^",
"@multiversx/sdk-core": "^12.4.3",
"@multiversx/sdk-extension-provider": "^2.0.7"
},
"bugs": {
"url": "https://github.com/logion-network/logion-api/issues"
},
"homepage": "https://github.com/logion-network/logion-api/packages/extension#readme",
"devDependencies": {
"@tsconfig/node18": "^1.0.1",
"@types/node": "^18.6.1",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"eslint": "^8.20.0",
"jasmine": "^4.3.0",
"jasmine-spec-reporter": "^7.0.0",
"moq.ts": "^9.0.2",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
},
"engines": {
"node": ">=16"
},
"stableVersion": "0.1.0",
"typedoc": {
"entryPoint": "./src/index.ts",
"displayName": "MultiversX"
}
}
36 changes: 36 additions & 0 deletions packages/multiversx/src/MultiversxSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { BaseSigner, SignAndSendFunction, TypedSignature } from "@logion/client";
import { ValidAccountId } from "@logion/node-api";
import { SignableMessage } from "@multiversx/sdk-core";
import { ExtensionProvider } from "@multiversx/sdk-extension-provider";

export class MultiversxSigner extends BaseSigner {

constructor() {
super();
this.provider = ExtensionProvider.getInstance();
}

private readonly provider: ExtensionProvider;

async login(): Promise<string> {
if (await this.provider.init()) {
return await this.provider.login();
} else {
throw new Error("No MultiversX account available");
}
}

async signToHex(_signerId: ValidAccountId, message: string): Promise<TypedSignature> {
const signableMessage = new SignableMessage({
message: Buffer.from(message.substring(2), "hex")
});
const signedMessage = await this.provider.signMessage(signableMessage);
const signature = '0x' + signedMessage.getSignature().toString('hex');
return { signature, type: "MULTIVERSX" }
}

buildSignAndSendFunction(): Promise<SignAndSendFunction> {
throw new Error("Cannot sign and send extrinsics with MultiversX signer");
}

}
1 change: 1 addition & 0 deletions packages/multiversx/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './MultiversxSigner.js';
9 changes: 9 additions & 0 deletions packages/multiversx/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./src/**/*"
]
}
Loading