Skip to content

Commit

Permalink
Merge pull request #170 from logion-network/feature/multiversx
Browse files Browse the repository at this point in the history
Support for MultiversX addresses, tokens and signatures.
  • Loading branch information
benoitdevos authored Jul 4, 2023
2 parents 9f895dc + 2b8ce76 commit 1babc8a
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 36 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.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

0 comments on commit 1babc8a

Please sign in to comment.