From 107c58f654ace360f484f73ff02ac78cbd91aec9 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 19 Aug 2024 13:23:55 +0200 Subject: [PATCH 01/21] Solana package skeleton --- packages/xchain-solana/CHANGELOG.md | 1 + packages/xchain-solana/README.md | 14 +++++++ packages/xchain-solana/__e2e__/client.ts | 3 ++ .../xchain-solana/__tests__/client.test.ts | 3 ++ packages/xchain-solana/package.json | 39 +++++++++++++++++++ packages/xchain-solana/rollup.config.js | 34 ++++++++++++++++ packages/xchain-solana/src/index.ts | 0 packages/xchain-solana/tsconfig.json | 6 +++ 8 files changed, 100 insertions(+) create mode 100644 packages/xchain-solana/CHANGELOG.md create mode 100644 packages/xchain-solana/README.md create mode 100644 packages/xchain-solana/__e2e__/client.ts create mode 100644 packages/xchain-solana/__tests__/client.test.ts create mode 100644 packages/xchain-solana/package.json create mode 100644 packages/xchain-solana/rollup.config.js create mode 100644 packages/xchain-solana/src/index.ts create mode 100644 packages/xchain-solana/tsconfig.json diff --git a/packages/xchain-solana/CHANGELOG.md b/packages/xchain-solana/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/packages/xchain-solana/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/xchain-solana/README.md b/packages/xchain-solana/README.md new file mode 100644 index 000000000..b1dac65b9 --- /dev/null +++ b/packages/xchain-solana/README.md @@ -0,0 +1,14 @@ +
+

Solana

+ +

+ + NPM Version + + + NPM Downloads + +

+
+ +
\ No newline at end of file diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts new file mode 100644 index 000000000..5967547dd --- /dev/null +++ b/packages/xchain-solana/__e2e__/client.ts @@ -0,0 +1,3 @@ +describe('Solana client', () => { + it('Test 1', () => {}) +}) diff --git a/packages/xchain-solana/__tests__/client.test.ts b/packages/xchain-solana/__tests__/client.test.ts new file mode 100644 index 000000000..5967547dd --- /dev/null +++ b/packages/xchain-solana/__tests__/client.test.ts @@ -0,0 +1,3 @@ +describe('Solana client', () => { + it('Test 1', () => {}) +}) diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json new file mode 100644 index 000000000..91ef10d76 --- /dev/null +++ b/packages/xchain-solana/package.json @@ -0,0 +1,39 @@ +{ + "name": "@xchainjs/xchain-solana", + "version": "0.0.1", + "description": "Solana client for XChainJS", + "keywords": [ + "Solana", + "XChain" + ], + "author": "THORChain", + "homepage": "https://github.com/xchainjs/xchainjs-lib", + "license": "MIT", + "main": "lib/index.js", + "module": "lib/index.esm.js", + "typings": "lib/index.d.ts", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git@github.com:xchainjs/xchainjs-lib.git" + }, + "scripts": { + "clean": "rm -rf .turbo && rm -rf lib", + "build": "yarn clean && rollup -c", + "build:release": "yarn exec rm -rf release && yarn pack && yarn exec \"mkdir release && tar zxvf package.tgz --directory release && rm package.tgz\"", + "test": "jest", + "e2e": "jest --config jest.config.e2e.js", + "lint": "eslint \"{src,__tests__}/**/*.ts\" --fix --max-warnings 0" + }, + "dependencies": {}, + "publishConfig": { + "access": "public", + "directory": "release/package" + } +} \ No newline at end of file diff --git a/packages/xchain-solana/rollup.config.js b/packages/xchain-solana/rollup.config.js new file mode 100644 index 000000000..97bf77e08 --- /dev/null +++ b/packages/xchain-solana/rollup.config.js @@ -0,0 +1,34 @@ +import commonjs from '@rollup/plugin-commonjs' +import json from '@rollup/plugin-json' +import resolve from '@rollup/plugin-node-resolve' +import typescript from 'rollup-plugin-typescript2' + +import pkg from './package.json' + +export default { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + exports: 'named', + sourcemap: true, + }, + { + file: pkg.module, + format: 'es', + exports: 'named', + sourcemap: true, + }, + ], + plugins: [ + json(), + resolve({ preferBuiltins: true, browser: true }), + typescript({ + exclude: '__tests__/**', + declarationDir: '.', + }), + commonjs(), + ], + external: Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.peerDependencies || {})), +} diff --git a/packages/xchain-solana/src/index.ts b/packages/xchain-solana/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/xchain-solana/tsconfig.json b/packages/xchain-solana/tsconfig.json new file mode 100644 index 000000000..bf144d0eb --- /dev/null +++ b/packages/xchain-solana/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*" + ] +} \ No newline at end of file From f38a06e25a2f877eafa34f79d92b52ceec182557 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Thu, 22 Aug 2024 00:43:54 +0200 Subject: [PATCH 02/21] Get and validate address functions --- packages/xchain-solana/__e2e__/client.ts | 20 +- .../xchain-solana/__tests__/client.test.ts | 18 +- packages/xchain-solana/jest.config.js | 6 + packages/xchain-solana/jest.setup.js | 1 + packages/xchain-solana/package.json | 11 +- packages/xchain-solana/src/client.ts | 81 +++ packages/xchain-solana/src/const.ts | 28 ++ packages/xchain-solana/src/index.ts | 4 + packages/xchain-solana/src/types.ts | 6 + yarn.lock | 476 +++++++++++++++++- 10 files changed, 638 insertions(+), 13 deletions(-) create mode 100644 packages/xchain-solana/jest.config.js create mode 100644 packages/xchain-solana/jest.setup.js create mode 100644 packages/xchain-solana/src/client.ts create mode 100644 packages/xchain-solana/src/const.ts create mode 100644 packages/xchain-solana/src/types.ts diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 5967547dd..7abf91033 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -1,3 +1,21 @@ +import { Client } from '../src' + describe('Solana client', () => { - it('Test 1', () => {}) + let client: Client + + beforeAll(() => { + client = new Client({ + phrase: process.env.PHRASE_MAINNET, + }) + }) + + it('Should get address with no index', async () => { + const address = await client.getAddressAsync() + console.log(address) + }) + + it('Should get address with index 1', async () => { + const address = await client.getAddressAsync(1) + console.log(address) + }) }) diff --git a/packages/xchain-solana/__tests__/client.test.ts b/packages/xchain-solana/__tests__/client.test.ts index 5967547dd..815e0ced1 100644 --- a/packages/xchain-solana/__tests__/client.test.ts +++ b/packages/xchain-solana/__tests__/client.test.ts @@ -1,3 +1,19 @@ +import { Client } from '../src' + describe('Solana client', () => { - it('Test 1', () => {}) + let client: Client + + beforeAll(() => { + client = new Client({ + phrase: process.env.PHRASE_MAINNET, + }) + }) + + it('Should validate address as valid', () => { + expect(client.validateAddress('G72oBA9cRYUzR8Q9oLvJcNRx5ovcDGFvHsbZKp1BT75W')).toBeTruthy() + }) + + it('Should validate address as invalid', () => { + expect(client.validateAddress('fakeAddress')).toBeFalsy() + }) }) diff --git a/packages/xchain-solana/jest.config.js b/packages/xchain-solana/jest.config.js new file mode 100644 index 000000000..2c5daf952 --- /dev/null +++ b/packages/xchain-solana/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/node_modules', '/lib'], + setupFilesAfterEnv: ['./jest.setup.js'], +} diff --git a/packages/xchain-solana/jest.setup.js b/packages/xchain-solana/jest.setup.js new file mode 100644 index 000000000..7f0aeddaa --- /dev/null +++ b/packages/xchain-solana/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(60000) diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json index 91ef10d76..16d999344 100644 --- a/packages/xchain-solana/package.json +++ b/packages/xchain-solana/package.json @@ -31,9 +31,16 @@ "e2e": "jest --config jest.config.e2e.js", "lint": "eslint \"{src,__tests__}/**/*.ts\" --fix --max-warnings 0" }, - "dependencies": {}, + "dependencies": { + "@solana/addresses": "2.0.0-rc.1", + "@solana/web3.js": "1.95.2", + "@xchainjs/xchain-client": "workspace:*", + "@xchainjs/xchain-crypto": "workspace:*", + "@xchainjs/xchain-util": "workspace:*", + "micro-ed25519-hdkey": "0.1.2" + }, "publishConfig": { "access": "public", "directory": "release/package" } -} \ No newline at end of file +} diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts new file mode 100644 index 000000000..83b51d35c --- /dev/null +++ b/packages/xchain-solana/src/client.ts @@ -0,0 +1,81 @@ +import { isAddress } from '@solana/addresses' +import { Keypair } from '@solana/web3.js' +import { AssetInfo, Balance, BaseXChainClient, Fees, PreparedTx, Tx, TxHash, TxsPage } from '@xchainjs/xchain-client' +import { getSeed } from '@xchainjs/xchain-crypto' +import { Address } from '@xchainjs/xchain-util' +import { HDKey } from 'micro-ed25519-hdkey' + +import { SOLChain, defaultSolanaParams } from './const' +import { SOLClientParams } from './types' + +export class Client extends BaseXChainClient { + constructor(params: SOLClientParams = defaultSolanaParams) { + super(SOLChain, { + ...defaultSolanaParams, + ...params, + }) + } + + getExplorerUrl(): string { + throw new Error('Method not implemented.') + } + + getExplorerAddressUrl(): string { + throw new Error('Method not implemented.') + } + + getExplorerTxUrl(): string { + throw new Error('Method not implemented.') + } + + public getAddress(index = 0): string { + if (!this.phrase) throw new Error('Phrase must be provided') + + const seed = getSeed(this.phrase) + const hd = HDKey.fromMasterSeed(seed.toString('hex')) + + const keypair = Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index || 0), true).privateKey) + + return keypair.publicKey.toBase58() + } + + public validateAddress(address: Address): boolean { + return isAddress(address) + } + + public async getAddressAsync(index?: number): Promise { + return this.getAddress(index) + } + + getFees(): Promise { + throw new Error('Method not implemented.') + } + + getBalance(): Promise { + throw new Error('Method not implemented.') + } + + getTransactions(): Promise { + throw new Error('Method not implemented.') + } + + getTransactionData(): Promise { + throw new Error('Method not implemented.') + } + + transfer(): Promise { + throw new Error('Method not implemented.') + } + + broadcastTx(): Promise { + throw new Error('Method not implemented.') + } + + getAssetInfo(): AssetInfo { + throw new Error('Method not implemented.') + } + + prepareTx(): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/xchain-solana/src/const.ts b/packages/xchain-solana/src/const.ts new file mode 100644 index 000000000..b2f913e6e --- /dev/null +++ b/packages/xchain-solana/src/const.ts @@ -0,0 +1,28 @@ +import { Network } from '@xchainjs/xchain-client' +import { Asset, AssetType } from '@xchainjs/xchain-util' + +import { SOLClientParams } from './types' + +/** + * Solana chain symbol + */ +export const SOLChain = 'SOL' as const + +/** + * Solana native asset + */ +export const SOLAsset: Asset = { + chain: 'SOL', + ticker: 'SOL', + symbol: 'SOL', + type: AssetType.NATIVE, +} + +export const defaultSolanaParams: SOLClientParams = { + network: Network.Mainnet, + rootDerivationPaths: { + [Network.Mainnet]: "m/44'/501'/0'/0/", + [Network.Testnet]: "m/44'/501'/0'/0/", + [Network.Stagenet]: "m/44'/501'/0'/0/", + }, +} diff --git a/packages/xchain-solana/src/index.ts b/packages/xchain-solana/src/index.ts index e69de29bb..00c823e9c 100644 --- a/packages/xchain-solana/src/index.ts +++ b/packages/xchain-solana/src/index.ts @@ -0,0 +1,4 @@ +export { Client } from './client' + +export * from './types' +export * from './const' diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts new file mode 100644 index 000000000..86c9d12f5 --- /dev/null +++ b/packages/xchain-solana/src/types.ts @@ -0,0 +1,6 @@ +import { XChainClientParams } from '@xchainjs/xchain-client' + +/** + * Solana client params + */ +export type SOLClientParams = XChainClientParams diff --git a/yarn.lock b/yarn.lock index 20bb7aa93..74100b63c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -503,6 +503,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.24.8": + version: 7.25.0 + resolution: "@babel/runtime@npm:7.25.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/bd3faf246170826cef2071a94d7b47b49d532351360ecd17722d03f6713fd93a3eb3dbd9518faa778d5e8ccad7392a7a604e56bd37aaad3f3aa68d619ccd983d + languageName: node + linkType: hard + "@babel/template@npm:^7.20.7, @babel/template@npm:^7.3.3": version: 7.20.7 resolution: "@babel/template@npm:7.20.7" @@ -2456,6 +2465,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:^1.4.2": + version: 1.5.0 + resolution: "@noble/curves@npm:1.5.0" + dependencies: + "@noble/hashes": "npm:1.4.0" + checksum: 10c0/89faed98e7ff1fee086777afcf63b7ec237121ebfe09495eb9ff7f73c7dd696000c795a24a1bedadc804b592d4b3c655f2e4a9fe9a3afe312a9e6376558d3737 + languageName: node + linkType: hard + "@noble/ed25519@npm:2.0.0": version: 2.0.0 resolution: "@noble/ed25519@npm:2.0.0" @@ -2463,6 +2481,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:~1.7.1": + version: 1.7.3 + resolution: "@noble/ed25519@npm:1.7.3" + checksum: 10c0/dc162c3be5ae5a3cc0e6aff8209c8d175f24bba22f2b473aa849e102471193c83664b06f0ba2b5e01e9aa1a67a44daf313f478adb9f38768408a8bcad6145a48 + languageName: node + linkType: hard + "@noble/hashes@npm:1.3.0, @noble/hashes@npm:^1.2.0": version: 1.3.0 resolution: "@noble/hashes@npm:1.3.0" @@ -2477,13 +2502,20 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.3": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:^1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" checksum: 10c0/8c3f005ee72e7b8f9cff756dfae1241485187254e3f743873e22073d63906863df5d4f13d441b7530ea614b7a093f0d889309f28b59850f33b66cb26a779a4a5 languageName: node linkType: hard +"@noble/hashes@npm:~1.1.1": + version: 1.1.5 + resolution: "@noble/hashes@npm:1.1.5" + checksum: 10c0/cf7f3baff1d95968309d260441d823a854dd76ca0b02f970db0c2f2e0de980c3cc55afcc6a8f3808bb505e65c59cd5332c5f32f821cf35b7982dc7570e991731 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3010,6 +3042,114 @@ __metadata: languageName: node linkType: hard +"@solana/addresses@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/addresses@npm:2.0.0-rc.1" + dependencies: + "@solana/assertions": "npm:2.0.0-rc.1" + "@solana/codecs-core": "npm:2.0.0-rc.1" + "@solana/codecs-strings": "npm:2.0.0-rc.1" + "@solana/errors": "npm:2.0.0-rc.1" + peerDependencies: + typescript: ">=5" + checksum: 10c0/3aabe755fd7fb7dee2edfd73faa15f83d97860d4f852bdbf6f90bec08909b4df7f623b1f714ff6951b05b01ed091bbffb7b68a76da0ccea238047b0610efe084 + languageName: node + linkType: hard + +"@solana/assertions@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/assertions@npm:2.0.0-rc.1" + dependencies: + "@solana/errors": "npm:2.0.0-rc.1" + peerDependencies: + typescript: ">=5" + checksum: 10c0/2c9b6881e80671813a58723441c4aee325255017602e1195f48bc9ba4b609da965b51e56835eb5897a7d1978a4e09749eb4a2aafddcf2b47a5bf949f4d906ac7 + languageName: node + linkType: hard + +"@solana/buffer-layout@npm:^4.0.1": + version: 4.0.1 + resolution: "@solana/buffer-layout@npm:4.0.1" + dependencies: + buffer: "npm:~6.0.3" + checksum: 10c0/6535f3908cf6dfc405b665795f0c2eaa0482a8c6b1811403945cf7b450e7eb7b40acce3e8af046f2fcc3eea1a15e61d48c418315d813bee4b720d56b00053305 + languageName: node + linkType: hard + +"@solana/codecs-core@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/codecs-core@npm:2.0.0-rc.1" + dependencies: + "@solana/errors": "npm:2.0.0-rc.1" + peerDependencies: + typescript: ">=5" + checksum: 10c0/3b1fd09727bf850d191292b14e1afb64cda4e57f898c06483f40d0402c4f07f1d4df555f028f664701e647834c74924818857443666d039f4e44c8c01f31f427 + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/codecs-numbers@npm:2.0.0-rc.1" + dependencies: + "@solana/codecs-core": "npm:2.0.0-rc.1" + "@solana/errors": "npm:2.0.0-rc.1" + peerDependencies: + typescript: ">=5" + checksum: 10c0/baf888bbd9c9ed2420207329c735def60a2b3d94d4a0dd1a92703f4de165a96dfd5b66e4fe954d6a7fae12b6b95c41da500499f100b6d5cfad6420d4bfe71b50 + languageName: node + linkType: hard + +"@solana/codecs-strings@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/codecs-strings@npm:2.0.0-rc.1" + dependencies: + "@solana/codecs-core": "npm:2.0.0-rc.1" + "@solana/codecs-numbers": "npm:2.0.0-rc.1" + "@solana/errors": "npm:2.0.0-rc.1" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5" + checksum: 10c0/7f3483407de7e324075a85f2f8c91103021d6b8f38cfd4cf78603cbd7b00ea8b828a0cb9b61fb2b0db6d3e733fdf358006de23278cf3b103af1f1de4f3f66233 + languageName: node + linkType: hard + +"@solana/errors@npm:2.0.0-rc.1": + version: 2.0.0-rc.1 + resolution: "@solana/errors@npm:2.0.0-rc.1" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + peerDependencies: + typescript: ">=5" + bin: + errors: bin/cli.mjs + checksum: 10c0/26b9edb43b4ba86b36aefb020a6e47706554ce57a95a357a55879c570ffd000417b1d9567b94120d114dfd38051e8362c18ee082b58cc34690c4c00f1040423c + languageName: node + linkType: hard + +"@solana/web3.js@npm:1.95.2": + version: 1.95.2 + resolution: "@solana/web3.js@npm:1.95.2" + dependencies: + "@babel/runtime": "npm:^7.24.8" + "@noble/curves": "npm:^1.4.2" + "@noble/hashes": "npm:^1.4.0" + "@solana/buffer-layout": "npm:^4.0.1" + agentkeepalive: "npm:^4.5.0" + bigint-buffer: "npm:^1.1.5" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + bs58: "npm:^4.0.1" + buffer: "npm:6.0.3" + fast-stable-stringify: "npm:^1.0.0" + jayson: "npm:^4.1.1" + node-fetch: "npm:^2.7.0" + rpc-websockets: "npm:^9.0.2" + superstruct: "npm:^2.0.2" + checksum: 10c0/cb43f9657b9ff00392c9e4d6168ed5cf4b0a1f8add0cb944e1153f4d3607ec46bffc9601eae84cf9f5231fcc553b5d7ddb98c186365b6ac2ca0cc9c8991c2605 + languageName: node + linkType: hard + "@substrate/ss58-registry@npm:^1.44.0": version: 1.48.0 resolution: "@substrate/ss58-registry@npm:1.48.0" @@ -3024,6 +3164,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:^0.5.11": + version: 0.5.12 + resolution: "@swc/helpers@npm:0.5.12" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/44693c0f34d772d63f3a6fb461964ec583055549a96df9790afec125b2ba06929a63cf9a165a9aaf22317779f460f8caafa94458b70d5cb2bc057b6ba9b5d02c + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -3168,6 +3317,15 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:^3.4.33": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + "@types/crypto-js@npm:^3.1.43": version: 3.1.47 resolution: "@types/crypto-js@npm:3.1.47" @@ -3347,7 +3505,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^12.12.47, @types/node@npm:^12.7.1": +"@types/node@npm:^12.12.47, @types/node@npm:^12.12.54, @types/node@npm:^12.7.1": version: 12.20.55 resolution: "@types/node@npm:12.20.55" checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 @@ -3456,6 +3614,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^8.3.4": + version: 8.3.4 + resolution: "@types/uuid@npm:8.3.4" + checksum: 10c0/b9ac98f82fcf35962317ef7dc44d9ac9e0f6fdb68121d384c88fe12ea318487d5585d3480fa003cf28be86a3bbe213ca688ba786601dce4a97724765eb5b1cf2 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.1": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" @@ -3470,6 +3635,24 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^7.4.4": + version: 7.4.7 + resolution: "@types/ws@npm:7.4.7" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/f1f53febd8623a85cef2652949acd19d83967e350ea15a851593e3033501750a1e04f418552e487db90a3d48611a1cff3ffcf139b94190c10f2fd1e1dc95ff10 + languageName: node + linkType: hard + +"@types/ws@npm:^8.2.2": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -4067,6 +4250,19 @@ __metadata: languageName: unknown linkType: soft +"@xchainjs/xchain-solana@workspace:packages/xchain-solana": + version: 0.0.0-use.local + resolution: "@xchainjs/xchain-solana@workspace:packages/xchain-solana" + dependencies: + "@solana/addresses": "npm:2.0.0-rc.1" + "@solana/web3.js": "npm:1.95.2" + "@xchainjs/xchain-client": "workspace:*" + "@xchainjs/xchain-crypto": "workspace:*" + "@xchainjs/xchain-util": "workspace:*" + micro-ed25519-hdkey: "npm:0.1.2" + languageName: unknown + linkType: soft + "@xchainjs/xchain-thorchain-amm@workspace:*, @xchainjs/xchain-thorchain-amm@workspace:packages/xchain-thorchain-amm": version: 0.0.0-use.local resolution: "@xchainjs/xchain-thorchain-amm@workspace:packages/xchain-thorchain-amm" @@ -4187,6 +4383,18 @@ __metadata: languageName: unknown linkType: soft +"JSONStream@npm:^1.3.5": + version: 1.3.5 + resolution: "JSONStream@npm:1.3.5" + dependencies: + jsonparse: "npm:^1.2.0" + through: "npm:>=2.2.7 <3" + bin: + JSONStream: ./bin.js + checksum: 10c0/0f54694da32224d57b715385d4a6b668d2117379d1f3223dc758459246cca58fdc4c628b83e8a8883334e454a0a30aa198ede77c788b55537c1844f686a751f2 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -4251,6 +4459,15 @@ __metadata: languageName: node linkType: hard +"agentkeepalive@npm:^4.5.0": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 10c0/394ea19f9710f230722996e156607f48fdf3a345133b0b1823244b7989426c16019a428b56c82d3eabef616e938812981d9009f4792ecc66bd6a59e991c62612 + languageName: node + linkType: hard + "aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" @@ -4799,6 +5016,16 @@ __metadata: languageName: node linkType: hard +"bigint-buffer@npm:^1.1.5": + version: 1.1.5 + resolution: "bigint-buffer@npm:1.1.5" + dependencies: + bindings: "npm:^1.3.0" + node-gyp: "npm:latest" + checksum: 10c0/aa41e53d38242a2f05f85b08eaf592635f92e5328822784cda518232b1644efdbf29ab3664951b174cc645848add4605488e25c9439bcc749660c885b4ff6118 + languageName: node + linkType: hard + "bignumber.js@npm:9.0.0": version: 9.0.0 resolution: "bignumber.js@npm:9.0.0" @@ -4992,6 +5219,17 @@ __metadata: languageName: node linkType: hard +"borsh@npm:^0.7.0": + version: 0.7.0 + resolution: "borsh@npm:0.7.0" + dependencies: + bn.js: "npm:^5.2.0" + bs58: "npm:^4.0.0" + text-encoding-utf-8: "npm:^1.0.2" + checksum: 10c0/513b3e51823d2bf5be77cec27742419d2b0427504825dd7ceb00dedb820f246a4762f04b83d5e3aa39c8e075b3cbaeb7ca3c90bd1cbeecccb4a510575be8c581 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -5185,6 +5423,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:6.0.3, buffer@npm:^6.0.3, buffer@npm:~6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + "buffer@npm:^5.2.1, buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -5195,13 +5443,13 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^6.0.3": - version: 6.0.3 - resolution: "buffer@npm:6.0.3" +"bufferutil@npm:^4.0.1": + version: 4.0.8 + resolution: "bufferutil@npm:4.0.8" dependencies: - base64-js: "npm:^1.3.1" - ieee754: "npm:^1.2.1" - checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/36cdc5b53a38d9f61f89fdbe62029a2ebcd020599862253fefebe31566155726df9ff961f41b8c97b02b4c12b391ef97faf94e2383392654cf8f0ed68f76e47c languageName: node linkType: hard @@ -5350,6 +5598,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 + languageName: node + linkType: hard + "change-case@npm:4.1.2": version: 4.1.2 resolution: "change-case@npm:4.1.2" @@ -5634,6 +5889,20 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + +"commander@npm:^2.20.3": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 + languageName: node + linkType: hard + "commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" @@ -6010,6 +6279,13 @@ __metadata: languageName: node linkType: hard +"delay@npm:^5.0.0": + version: 5.0.0 + resolution: "delay@npm:5.0.0" + checksum: 10c0/01cdc4cd0cd35fb622518a3df848e67e09763a38e7cdada2232b6fda9ddda72eddcf74f0e24211200fbe718434f2335f2a2633875a6c96037fefa6de42896ad7 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -6397,6 +6673,22 @@ __metadata: languageName: node linkType: hard +"es6-promise@npm:^4.0.3": + version: 4.2.8 + resolution: "es6-promise@npm:4.2.8" + checksum: 10c0/2373d9c5e9a93bdd9f9ed32ff5cb6dd3dd785368d1c21e9bbbfd07d16345b3774ae260f2bd24c8f836a6903f432b4151e7816a7fa8891ccb4e1a55a028ec42c3 + languageName: node + linkType: hard + +"es6-promisify@npm:^5.0.0": + version: 5.0.0 + resolution: "es6-promisify@npm:5.0.0" + dependencies: + es6-promise: "npm:^4.0.3" + checksum: 10c0/23284c6a733cbf7842ec98f41eac742c9f288a78753c4fe46652bae826446ced7615b9e8a5c5f121a08812b1cd478ea58630f3e1c3d70835bd5dcd69c7cd75c9 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -6684,6 +6976,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "events@npm:^3.0.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -6788,6 +7087,13 @@ __metadata: languageName: node linkType: hard +"eyes@npm:^0.1.8": + version: 0.1.8 + resolution: "eyes@npm:0.1.8" + checksum: 10c0/4c79a9cbf45746d8c9f48cc957e35ad8ea336add1c7b8d5a0e002efc791a7a62b27b2188184ef1a1eea7bc3cd06b161791421e0e6c5fe78309705a162c53eea8 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6836,6 +7142,13 @@ __metadata: languageName: node linkType: hard +"fast-stable-stringify@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-stable-stringify@npm:1.0.0" + checksum: 10c0/1d773440c7a9615950577665074746c2e92edafceefa789616ecb6166229e0ccc6dae206ca9b9f7da0d274ba5779162aab2d07940a0f6e52a41a4e555392eb3b + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -7588,6 +7901,15 @@ __metadata: languageName: node linkType: hard +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 10c0/f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a + languageName: node + linkType: hard + "husky@npm:^8.0.0": version: 8.0.3 resolution: "husky@npm:8.0.3" @@ -8182,6 +8504,28 @@ __metadata: languageName: node linkType: hard +"jayson@npm:^4.1.1": + version: 4.1.1 + resolution: "jayson@npm:4.1.1" + dependencies: + "@types/connect": "npm:^3.4.33" + "@types/node": "npm:^12.12.54" + "@types/ws": "npm:^7.4.4" + JSONStream: "npm:^1.3.5" + commander: "npm:^2.20.3" + delay: "npm:^5.0.0" + es6-promisify: "npm:^5.0.0" + eyes: "npm:^0.1.8" + isomorphic-ws: "npm:^4.0.1" + json-stringify-safe: "npm:^5.0.1" + uuid: "npm:^8.3.2" + ws: "npm:^7.5.10" + bin: + jayson: bin/jayson.js + checksum: 10c0/cee6fd5c2b432514955846981b96bd7359356ea3e839206996c8371dca1801ad3e2b685913c7f6519f701cd3c35b8c99b0b40f69fb3218fcbb92962cfa7fd665 + languageName: node + linkType: hard + "jest-changed-files@npm:^29.5.0": version: 29.5.0 resolution: "jest-changed-files@npm:29.5.0" @@ -8765,6 +9109,13 @@ __metadata: languageName: node linkType: hard +"jsonparse@npm:^1.2.0": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 10c0/89bc68080cd0a0e276d4b5ab1b79cacd68f562467008d176dc23e16e97d4efec9e21741d92ba5087a8433526a45a7e6a9d5ef25408696c402ca1cfbc01a90bf0 + languageName: node + linkType: hard + "keccak@npm:^3.0.3": version: 3.0.4 resolution: "keccak@npm:3.0.4" @@ -9176,6 +9527,16 @@ __metadata: languageName: node linkType: hard +"micro-ed25519-hdkey@npm:0.1.2": + version: 0.1.2 + resolution: "micro-ed25519-hdkey@npm:0.1.2" + dependencies: + "@noble/ed25519": "npm:~1.7.1" + "@noble/hashes": "npm:~1.1.1" + checksum: 10c0/d0d266252d69953043ba2decf53b2c73df1ffd3f87ca84cd35b5568196c80cf8a1674d0caa2744709e7ba0f30b0648ca2784fcba334eda68a72fb329019d939e + languageName: node + linkType: hard + "micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -9438,6 +9799,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:^2.0.0": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + "mute-stream@npm:0.0.8": version: 0.0.8 resolution: "mute-stream@npm:0.0.8" @@ -9595,6 +9963,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -10846,6 +11228,28 @@ __metadata: languageName: unknown linkType: soft +"rpc-websockets@npm:^9.0.2": + version: 9.0.2 + resolution: "rpc-websockets@npm:9.0.2" + dependencies: + "@swc/helpers": "npm:^0.5.11" + "@types/uuid": "npm:^8.3.4" + "@types/ws": "npm:^8.2.2" + buffer: "npm:^6.0.3" + bufferutil: "npm:^4.0.1" + eventemitter3: "npm:^5.0.1" + utf-8-validate: "npm:^5.0.2" + uuid: "npm:^8.3.2" + ws: "npm:^8.5.0" + dependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/2f3b104a41f353a0c4e6f94474b347b42ea8c917d918259ea5b69878d082c2b8f83bdf5a9038714f536641700dbf7f102ce0c50505345fd8d61ee5d42dd72799 + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -11593,6 +11997,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^2.0.2": + version: 2.0.2 + resolution: "superstruct@npm:2.0.2" + checksum: 10c0/c6853db5240b4920f47b3c864dd1e23ede6819ea399ad29a65387d746374f6958c5f1c5b7e5bb152d9db117a74973e5005056d9bb83c24e26f18ec6bfae4a718 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -11691,6 +12102,13 @@ __metadata: languageName: node linkType: hard +"text-encoding-utf-8@npm:^1.0.2": + version: 1.0.2 + resolution: "text-encoding-utf-8@npm:1.0.2" + checksum: 10c0/87a64b394c850e8387c2ca7fc6929a26ce97fb598f1c55cd0fdaec4b8e2c3ed6770f65b2f3309c9175ef64ac5e403c8e48b53ceeb86d2897940c5e19cc00bb99 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -11708,7 +12126,7 @@ __metadata: languageName: node linkType: hard -"through@npm:^2.3.6, through@npm:^2.3.8": +"through@npm:>=2.2.7 <3, through@npm:^2.3.6, through@npm:^2.3.8": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10c0/4b09f3774099de0d4df26d95c5821a62faee32c7e96fb1f4ebd54a2d7c11c57fe88b0a0d49cf375de5fee5ae6bf4eb56dbbf29d07366864e2ee805349970d3cc @@ -12329,6 +12747,16 @@ __metadata: languageName: node linkType: hard +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/23cd6adc29e6901aa37ff97ce4b81be9238d0023c5e217515b34792f3c3edb01470c3bd6b264096dd73d0b01a1690b57468de3a24167dd83004ff71c51cc025f + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -12684,6 +13112,36 @@ __metadata: languageName: node linkType: hard +"ws@npm:^7.5.10": + version: 7.5.10 + resolution: "ws@npm:7.5.10" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/bd7d5f4aaf04fae7960c23dcb6c6375d525e00f795dd20b9385902bd008c40a94d3db3ce97d878acc7573df852056ca546328b27b39f47609f80fb22a0a9b61d + languageName: node + linkType: hard + +"ws@npm:^8.5.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06 + languageName: node + linkType: hard + "xchainjs-check-tx@workspace:examples/check-tx": version: 0.0.0-use.local resolution: "xchainjs-check-tx@workspace:examples/check-tx" From acecfba47dbedbd98a0608735cd5a3fe4453aa1e Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Thu, 22 Aug 2024 11:29:23 +0200 Subject: [PATCH 03/21] Derivation path bug fix --- packages/xchain-solana/src/client.ts | 35 ++++++++++++++++++++++++---- packages/xchain-solana/src/const.ts | 6 ++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 83b51d35c..0914ec77c 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -28,23 +28,50 @@ export class Client extends BaseXChainClient { throw new Error('Method not implemented.') } - public getAddress(index = 0): string { + /** + * Get the current address asynchronously. + * @param {number} index The index of the address. + * @returns {Address} The Solana address related to the index provided. + * @throws {"Phrase must be provided"} Thrown if the phrase has not been set before. + */ + public async getAddressAsync(index?: number): Promise { if (!this.phrase) throw new Error('Phrase must be provided') const seed = getSeed(this.phrase) const hd = HDKey.fromMasterSeed(seed.toString('hex')) - const keypair = Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index || 0), true).privateKey) + const keypair = Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index || 0)).privateKey) return keypair.publicKey.toBase58() } + /** + * Get the current address synchronously. + * @deprecated + */ + public getAddress(): string { + throw Error('Sync method not supported') + } + + /** + * Validate the given Solana address. + * @param {string} address Solana address to validate. + * @returns {boolean} `true` if the address is valid, `false` otherwise. + */ public validateAddress(address: Address): boolean { return isAddress(address) } - public async getAddressAsync(index?: number): Promise { - return this.getAddress(index) + /** + * Get the full derivation path based on the wallet index. + * @param {number} walletIndex The HD wallet index + * @returns {string} The full derivation path + */ + public getFullDerivationPath(walletIndex: number): string { + if (!this.rootDerivationPaths) { + throw Error('Can not generate derivation path due to root derivation path is undefined') + } + return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` } getFees(): Promise { diff --git a/packages/xchain-solana/src/const.ts b/packages/xchain-solana/src/const.ts index b2f913e6e..b9c6aaa7e 100644 --- a/packages/xchain-solana/src/const.ts +++ b/packages/xchain-solana/src/const.ts @@ -21,8 +21,8 @@ export const SOLAsset: Asset = { export const defaultSolanaParams: SOLClientParams = { network: Network.Mainnet, rootDerivationPaths: { - [Network.Mainnet]: "m/44'/501'/0'/0/", - [Network.Testnet]: "m/44'/501'/0'/0/", - [Network.Stagenet]: "m/44'/501'/0'/0/", + [Network.Mainnet]: "m/44'/501'/", + [Network.Testnet]: "m/44'/501'/", + [Network.Stagenet]: "m/44'/501'/", }, } From f23e1de2e64ad6d2425b3ec5913667dac1b0fa23 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Thu, 22 Aug 2024 14:03:32 +0200 Subject: [PATCH 04/21] Explorer functions --- packages/xchain-solana/__e2e__/client.ts | 3 +- .../xchain-solana/__tests__/client.test.ts | 83 ++++++++++++++++++- packages/xchain-solana/src/client.ts | 44 ++++++++-- packages/xchain-solana/src/const.ts | 17 +++- packages/xchain-solana/src/types.ts | 6 +- 5 files changed, 141 insertions(+), 12 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 7abf91033..7df270de9 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -1,10 +1,11 @@ -import { Client } from '../src' +import { Client, defaultSolanaParams } from '../src' describe('Solana client', () => { let client: Client beforeAll(() => { client = new Client({ + ...defaultSolanaParams, phrase: process.env.PHRASE_MAINNET, }) }) diff --git a/packages/xchain-solana/__tests__/client.test.ts b/packages/xchain-solana/__tests__/client.test.ts index 815e0ced1..98d38cfec 100644 --- a/packages/xchain-solana/__tests__/client.test.ts +++ b/packages/xchain-solana/__tests__/client.test.ts @@ -1,10 +1,13 @@ -import { Client } from '../src' +import { Network } from '@xchainjs/xchain-client' + +import { Client, defaultSolanaParams } from '../src' describe('Solana client', () => { let client: Client beforeAll(() => { client = new Client({ + ...defaultSolanaParams, phrase: process.env.PHRASE_MAINNET, }) }) @@ -16,4 +19,82 @@ describe('Solana client', () => { it('Should validate address as invalid', () => { expect(client.validateAddress('fakeAddress')).toBeFalsy() }) + + describe('Explorers', () => { + describe('Mainnet', () => { + let client: Client + beforeAll(() => { + client = new Client() + }) + it('Should get explorer url', () => { + expect(client.getExplorerUrl()).toBe('https://explorer.solana.com/') + }) + it('Should get address url', () => { + expect(client.getExplorerAddressUrl('DkyQEQYdZ61vrRpPUHyyDNz9NBCcvVxSQjBqBcJvGdFT')).toBe( + 'https://explorer.solana.com/address/DkyQEQYdZ61vrRpPUHyyDNz9NBCcvVxSQjBqBcJvGdFT', + ) + }) + it('Should get transaction url', () => { + expect( + client.getExplorerTxUrl( + 'qnZzcK9tWBtgTbSirrCKZ59AYmZa6Dr9qQ6Ned1a4hxa171wevawknoyiBUz9cu3HssUw9W11JRVTFtwwJo3mxS', + ), + ).toBe( + 'https://explorer.solana.com/tx/qnZzcK9tWBtgTbSirrCKZ59AYmZa6Dr9qQ6Ned1a4hxa171wevawknoyiBUz9cu3HssUw9W11JRVTFtwwJo3mxS', + ) + }) + }) + describe('Testnet', () => { + let client: Client + beforeAll(() => { + client = new Client({ + ...defaultSolanaParams, + network: Network.Testnet, + }) + }) + it('Should get explorer url', () => { + expect(client.getExplorerUrl()).toBe('https://explorer.solana.com/?cluster=testnet') + }) + it('Should get address url', () => { + expect(client.getExplorerAddressUrl('AKYxN3oot9NTMq1W1eG1nHD4BaVnhR8rsYzDcqpkWSjD')).toBe( + 'https://explorer.solana.com/address/AKYxN3oot9NTMq1W1eG1nHD4BaVnhR8rsYzDcqpkWSjD?cluster=testnet', + ) + }) + it('Should get transaction url', () => { + expect( + client.getExplorerTxUrl( + 'rAzjs4YRHHm6qSGe3EZzfwfW4WUpDx6RyK11iPL752Z1nAzGQCdxdCbSo6ZDKdwYZXuPojE1fwtirGjVvLLXPub', + ), + ).toBe( + 'https://explorer.solana.com/tx/rAzjs4YRHHm6qSGe3EZzfwfW4WUpDx6RyK11iPL752Z1nAzGQCdxdCbSo6ZDKdwYZXuPojE1fwtirGjVvLLXPub?cluster=testnet', + ) + }) + }) + describe('Stagenet', () => { + let client: Client + beforeAll(() => { + client = new Client({ + ...defaultSolanaParams, + network: Network.Stagenet, + }) + }) + it('Should get explorer url', () => { + expect(client.getExplorerUrl()).toBe('https://explorer.solana.com/') + }) + it('Should get address url', () => { + expect(client.getExplorerAddressUrl('DkyQEQYdZ61vrRpPUHyyDNz9NBCcvVxSQjBqBcJvGdFT')).toBe( + 'https://explorer.solana.com/address/DkyQEQYdZ61vrRpPUHyyDNz9NBCcvVxSQjBqBcJvGdFT', + ) + }) + it('Should get transaction url', () => { + expect( + client.getExplorerTxUrl( + 'qnZzcK9tWBtgTbSirrCKZ59AYmZa6Dr9qQ6Ned1a4hxa171wevawknoyiBUz9cu3HssUw9W11JRVTFtwwJo3mxS', + ), + ).toBe( + 'https://explorer.solana.com/tx/qnZzcK9tWBtgTbSirrCKZ59AYmZa6Dr9qQ6Ned1a4hxa171wevawknoyiBUz9cu3HssUw9W11JRVTFtwwJo3mxS', + ) + }) + }) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 0914ec77c..a95d8ae6e 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -1,6 +1,16 @@ import { isAddress } from '@solana/addresses' import { Keypair } from '@solana/web3.js' -import { AssetInfo, Balance, BaseXChainClient, Fees, PreparedTx, Tx, TxHash, TxsPage } from '@xchainjs/xchain-client' +import { + AssetInfo, + Balance, + BaseXChainClient, + ExplorerProviders, + Fees, + PreparedTx, + Tx, + TxHash, + TxsPage, +} from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' import { Address } from '@xchainjs/xchain-util' import { HDKey } from 'micro-ed25519-hdkey' @@ -9,27 +19,47 @@ import { SOLChain, defaultSolanaParams } from './const' import { SOLClientParams } from './types' export class Client extends BaseXChainClient { + private explorerProviders: ExplorerProviders constructor(params: SOLClientParams = defaultSolanaParams) { super(SOLChain, { ...defaultSolanaParams, ...params, }) + this.explorerProviders = params.explorerProviders } - getExplorerUrl(): string { - throw new Error('Method not implemented.') + /** + * Get the explorer URL. + * + * @returns {string} The explorer URL. + */ + public getExplorerUrl(): string { + return this.explorerProviders[this.getNetwork()].getExplorerUrl() } - getExplorerAddressUrl(): string { - throw new Error('Method not implemented.') + /** + * Get the explorer url for the given address. + * + * @param {Address} address + * @returns {string} The explorer url for the given address. + */ + public getExplorerAddressUrl(address: Address): string { + return this.explorerProviders[this.getNetwork()].getExplorerAddressUrl(address) } - getExplorerTxUrl(): string { - throw new Error('Method not implemented.') + /** + * Get the explorer url for the given transaction id. + * + * @param {string} txID + * @returns {string} The explorer url for the given transaction id. + */ + public getExplorerTxUrl(txID: TxHash): string { + return this.explorerProviders[this.getNetwork()].getExplorerTxUrl(txID) } /** * Get the current address asynchronously. + * * @param {number} index The index of the address. * @returns {Address} The Solana address related to the index provided. * @throws {"Phrase must be provided"} Thrown if the phrase has not been set before. diff --git a/packages/xchain-solana/src/const.ts b/packages/xchain-solana/src/const.ts index b9c6aaa7e..242a665c8 100644 --- a/packages/xchain-solana/src/const.ts +++ b/packages/xchain-solana/src/const.ts @@ -1,4 +1,4 @@ -import { Network } from '@xchainjs/xchain-client' +import { ExplorerProvider, Network } from '@xchainjs/xchain-client' import { Asset, AssetType } from '@xchainjs/xchain-util' import { SOLClientParams } from './types' @@ -18,6 +18,12 @@ export const SOLAsset: Asset = { type: AssetType.NATIVE, } +const mainnetExplorer = new ExplorerProvider( + 'https://explorer.solana.com/', + 'https://explorer.solana.com/address/%%ADDRESS%%', + 'https://explorer.solana.com/tx/%%TX_ID%%', +) + export const defaultSolanaParams: SOLClientParams = { network: Network.Mainnet, rootDerivationPaths: { @@ -25,4 +31,13 @@ export const defaultSolanaParams: SOLClientParams = { [Network.Testnet]: "m/44'/501'/", [Network.Stagenet]: "m/44'/501'/", }, + explorerProviders: { + [Network.Mainnet]: mainnetExplorer, + [Network.Testnet]: new ExplorerProvider( + 'https://explorer.solana.com/?cluster=testnet', + 'https://explorer.solana.com/address/%%ADDRESS%%?cluster=testnet', + 'https://explorer.solana.com/tx/%%TX_ID%%?cluster=testnet', + ), + [Network.Stagenet]: mainnetExplorer, + }, } diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index 86c9d12f5..a2b256a0f 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -1,6 +1,8 @@ -import { XChainClientParams } from '@xchainjs/xchain-client' +import { ExplorerProviders, XChainClientParams } from '@xchainjs/xchain-client' /** * Solana client params */ -export type SOLClientParams = XChainClientParams +export type SOLClientParams = XChainClientParams & { + explorerProviders: ExplorerProviders +} From 1f7b1f3df8b84da62b99b24d72b8fe312e7c8f31 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Thu, 22 Aug 2024 19:50:46 +0200 Subject: [PATCH 05/21] getBalance function skeleton --- packages/xchain-solana/__e2e__/client.ts | 10 + packages/xchain-solana/package.json | 1 + packages/xchain-solana/src/client.ts | 56 ++++- packages/xchain-solana/src/const.ts | 5 + packages/xchain-solana/src/solana-types.ts | 21 ++ packages/xchain-solana/src/types.ts | 8 +- packages/xchain-solana/src/utils.ts | 12 + yarn.lock | 271 ++++++++++++++++++++- 8 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 packages/xchain-solana/src/solana-types.ts create mode 100644 packages/xchain-solana/src/utils.ts diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 7df270de9..bcbe6f86e 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -1,3 +1,5 @@ +import { assetToString, baseToAsset } from '@xchainjs/xchain-util' + import { Client, defaultSolanaParams } from '../src' describe('Solana client', () => { @@ -19,4 +21,12 @@ describe('Solana client', () => { const address = await client.getAddressAsync(1) console.log(address) }) + + it('Should get address balance', async () => { + const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') + + balances.forEach((balance) => { + console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) + }) + }) }) diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json index 16d999344..92097f75a 100644 --- a/packages/xchain-solana/package.json +++ b/packages/xchain-solana/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@solana/addresses": "2.0.0-rc.1", + "@solana/spl-token": "0.4.8", "@solana/web3.js": "1.95.2", "@xchainjs/xchain-client": "workspace:*", "@xchainjs/xchain-crypto": "workspace:*", diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index a95d8ae6e..041c7b4c3 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -1,5 +1,6 @@ import { isAddress } from '@solana/addresses' -import { Keypair } from '@solana/web3.js' +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Connection, Keypair, PublicKey, clusterApiUrl } from '@solana/web3.js' import { AssetInfo, Balance, @@ -12,20 +13,37 @@ import { TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' -import { Address } from '@xchainjs/xchain-util' +import { Address, assetFromStringEx, baseAmount } from '@xchainjs/xchain-util' import { HDKey } from 'micro-ed25519-hdkey' -import { SOLChain, defaultSolanaParams } from './const' +import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' +import { TokenAssetData } from './solana-types' import { SOLClientParams } from './types' +import { getSolanaNetwork } from './utils' export class Client extends BaseXChainClient { private explorerProviders: ExplorerProviders + private connection: Connection + constructor(params: SOLClientParams = defaultSolanaParams) { super(SOLChain, { ...defaultSolanaParams, ...params, }) this.explorerProviders = params.explorerProviders + this.connection = new Connection(clusterApiUrl(getSolanaNetwork(this.getNetwork()))) + } + + /** + * Get information about the native asset of the Solana. + * + * @returns {AssetInfo} Information about the native asset. + */ + public getAssetInfo(): AssetInfo { + return { + asset: SOLAsset, + decimal: SOL_DECIMALS, + } } /** @@ -104,11 +122,33 @@ export class Client extends BaseXChainClient { return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` } - getFees(): Promise { - throw new Error('Method not implemented.') + public async getBalance(address: Address): Promise { + const balances: Balance[] = [] + + const nativeBalance = await this.connection.getBalance(new PublicKey(address)) + + balances.push({ + asset: SOLAsset, + amount: baseAmount(nativeBalance, SOL_DECIMALS), + }) + + const tokenBalances = await this.connection.getParsedTokenAccountsByOwner(new PublicKey(address), { + programId: TOKEN_PROGRAM_ID, + }) + + tokenBalances.value.forEach((balance) => { + const parsedData = balance.account.data.parsed as TokenAssetData + const symbol = 'TBD' // TODO: Find a way to retrieve symbol data + balances.push({ + amount: baseAmount(parsedData.info.tokenAmount.amount, parsedData.info.tokenAmount.decimals), + asset: assetFromStringEx(`SOL.${symbol}-${parsedData.info.mint}`), + }) + }) + + return balances } - getBalance(): Promise { + getFees(): Promise { throw new Error('Method not implemented.') } @@ -128,10 +168,6 @@ export class Client extends BaseXChainClient { throw new Error('Method not implemented.') } - getAssetInfo(): AssetInfo { - throw new Error('Method not implemented.') - } - prepareTx(): Promise { throw new Error('Method not implemented.') } diff --git a/packages/xchain-solana/src/const.ts b/packages/xchain-solana/src/const.ts index 242a665c8..033cdad5c 100644 --- a/packages/xchain-solana/src/const.ts +++ b/packages/xchain-solana/src/const.ts @@ -8,6 +8,11 @@ import { SOLClientParams } from './types' */ export const SOLChain = 'SOL' as const +/** + * Solana native asset decimals + */ +export const SOL_DECIMALS = 9 + /** * Solana native asset */ diff --git a/packages/xchain-solana/src/solana-types.ts b/packages/xchain-solana/src/solana-types.ts new file mode 100644 index 000000000..c0d2bbabe --- /dev/null +++ b/packages/xchain-solana/src/solana-types.ts @@ -0,0 +1,21 @@ +import { Address } from '@solana/addresses' + +type TokenAmount = { + amount: string + decimals: number + uiAmount: number + uiAmountString: number +} + +type Info = { + isNative: false + mint: Address + owner: Address + state: 'initialized' + tokenAmount: TokenAmount +} + +export type TokenAssetData = { + info: Info + type: 'account' +} diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index a2b256a0f..0ccff085f 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -1,8 +1,12 @@ -import { ExplorerProviders, XChainClientParams } from '@xchainjs/xchain-client' - +import { Balance as BaseBalance, ExplorerProviders, XChainClientParams } from '@xchainjs/xchain-client' +import { Asset, TokenAsset } from '@xchainjs/xchain-util' /** * Solana client params */ export type SOLClientParams = XChainClientParams & { explorerProviders: ExplorerProviders } + +export type Balance = BaseBalance & { + asset: Asset | TokenAsset +} diff --git a/packages/xchain-solana/src/utils.ts b/packages/xchain-solana/src/utils.ts new file mode 100644 index 000000000..b4e90fbfb --- /dev/null +++ b/packages/xchain-solana/src/utils.ts @@ -0,0 +1,12 @@ +import { Cluster } from '@solana/web3.js' +import { Network } from '@xchainjs/xchain-client' + +export const getSolanaNetwork = (network: Network): Cluster => { + switch (network) { + case Network.Mainnet: + case Network.Stagenet: + return 'mainnet-beta' + case Network.Testnet: + return 'testnet' + } +} diff --git a/yarn.lock b/yarn.lock index 74100b63c..60eea6f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -512,6 +512,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/runtime@npm:7.25.4" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/33e937e685f0bfc2d40c219261e2e50d0df7381a6e7cbf56b770e0c5d77cb0c21bf4d97da566cf0164317ed7508e992082c7b6cce7aaa3b17da5794f93fbfb46 + languageName: node + linkType: hard + "@babel/template@npm:^7.20.7, @babel/template@npm:^7.3.3": version: 7.20.7 resolution: "@babel/template@npm:7.20.7" @@ -3067,7 +3076,19 @@ __metadata: languageName: node linkType: hard -"@solana/buffer-layout@npm:^4.0.1": +"@solana/buffer-layout-utils@npm:^0.2.0": + version: 0.2.0 + resolution: "@solana/buffer-layout-utils@npm:0.2.0" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/web3.js": "npm:^1.32.0" + bigint-buffer: "npm:^1.1.5" + bignumber.js: "npm:^9.0.1" + checksum: 10c0/ed093999d7c0f93527a9b261a9a2a59e10b5ef78fc416fa896b86036fb4dadf923d17db68bffdc3e91eadecdb8b8cddd8ee37f12429980fcaba321e7b8a35d27 + languageName: node + linkType: hard + +"@solana/buffer-layout@npm:^4.0.0, @solana/buffer-layout@npm:^4.0.1": version: 4.0.1 resolution: "@solana/buffer-layout@npm:4.0.1" dependencies: @@ -3076,6 +3097,26 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-core@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/codecs-core@npm:2.0.0-preview.2" + dependencies: + "@solana/errors": "npm:2.0.0-preview.2" + checksum: 10c0/545340b797bbf5bfbc79c2ca66aa87265b04632e3abd4ec042b7e8e92ed5738726c75a8c8dffd3ea6c793b4f8cd0c74caac72fb41a49a17132f2bb194ab8722f + languageName: node + linkType: hard + +"@solana/codecs-core@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/codecs-core@npm:2.0.0-preview.4" + dependencies: + "@solana/errors": "npm:2.0.0-preview.4" + peerDependencies: + typescript: ">=5" + checksum: 10c0/fe6d52cec3b747a8ecf752675b57e7f01c5c1d6287d0ec339b49979afba0c37f6c357f4b0b75edb9a6f7876134ba8c1eb77daa3df5b3cdfc938fdebde5d98278 + languageName: node + linkType: hard + "@solana/codecs-core@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-core@npm:2.0.0-rc.1" @@ -3087,6 +3128,52 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-data-structures@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/codecs-data-structures@npm:2.0.0-preview.2" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.2" + "@solana/codecs-numbers": "npm:2.0.0-preview.2" + "@solana/errors": "npm:2.0.0-preview.2" + checksum: 10c0/b06e1cef79305ef3328db7a641e9f8303611e173d99b47ad0934f6793d69fa876e274c9b91373c00c80166fbec74fb3439d6198c8461a6b174e0b808391825bd + languageName: node + linkType: hard + +"@solana/codecs-data-structures@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/codecs-data-structures@npm:2.0.0-preview.4" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.4" + "@solana/codecs-numbers": "npm:2.0.0-preview.4" + "@solana/errors": "npm:2.0.0-preview.4" + peerDependencies: + typescript: ">=5" + checksum: 10c0/9274fbfe029654e6f3cf43a8e82c575c6fab406f6db6b77ee443a2288abf730c4c3c2defd479f9fc4e3d0b9a4334018ba821b4bddcd546586f99e4f778004c33 + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/codecs-numbers@npm:2.0.0-preview.2" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.2" + "@solana/errors": "npm:2.0.0-preview.2" + checksum: 10c0/8a2275085dd25ff106ba20d42f7769648ef3efcff5516725a69af08c48c99c2a9224de9b4a0d1ccf51bb463fc48a54b6b82853d4d8a31ba21648a2daff56c373 + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/codecs-numbers@npm:2.0.0-preview.4" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.4" + "@solana/errors": "npm:2.0.0-preview.4" + peerDependencies: + typescript: ">=5" + checksum: 10c0/02ea792e055f6c798ca140d00d429e0ae6d37b9b42ff3ad950dba98666e18324eeaabd13cdcc584c64a21b10ef3706aaeb23691ba53520dba462fbc071bd63aa + languageName: node + linkType: hard + "@solana/codecs-numbers@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-numbers@npm:2.0.0-rc.1" @@ -3099,6 +3186,33 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-strings@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/codecs-strings@npm:2.0.0-preview.2" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.2" + "@solana/codecs-numbers": "npm:2.0.0-preview.2" + "@solana/errors": "npm:2.0.0-preview.2" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + checksum: 10c0/76f741de87bb8b95912467981fe3263d10cc12c3ba66f31d60c28474de57170b7ba0d0f1f6ee9ce6902a137433787a6fc72395ce451a08809207e4d85a621729 + languageName: node + linkType: hard + +"@solana/codecs-strings@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/codecs-strings@npm:2.0.0-preview.4" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.4" + "@solana/codecs-numbers": "npm:2.0.0-preview.4" + "@solana/errors": "npm:2.0.0-preview.4" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5" + checksum: 10c0/c1db4d3d2989390461ddf5ae3a08ac25b2b520290565b46469db1fade0600aea4adf501f3c683ef46baabc25cdd9f4e857b267e0cde91f6de4417efc9b0b9ce3 + languageName: node + linkType: hard + "@solana/codecs-strings@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-strings@npm:2.0.0-rc.1" @@ -3113,6 +3227,60 @@ __metadata: languageName: node linkType: hard +"@solana/codecs@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/codecs@npm:2.0.0-preview.2" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.2" + "@solana/codecs-data-structures": "npm:2.0.0-preview.2" + "@solana/codecs-numbers": "npm:2.0.0-preview.2" + "@solana/codecs-strings": "npm:2.0.0-preview.2" + "@solana/options": "npm:2.0.0-preview.2" + checksum: 10c0/50f8177c042b1a34c83b66ecd0ff67cb0713dc3c4fbac6988d14224ee784d25d626a791eec74a77b60a83be0b5b708832c09067bcee9bad2eede19a0b77c6a82 + languageName: node + linkType: hard + +"@solana/codecs@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/codecs@npm:2.0.0-preview.4" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.4" + "@solana/codecs-data-structures": "npm:2.0.0-preview.4" + "@solana/codecs-numbers": "npm:2.0.0-preview.4" + "@solana/codecs-strings": "npm:2.0.0-preview.4" + "@solana/options": "npm:2.0.0-preview.4" + peerDependencies: + typescript: ">=5" + checksum: 10c0/49a736614a6a4b9747cd040bd61537cbdd58940ed4cdb509d601a1dd05f33f6407bc7d3d8b894962b4a47c59df561098ce50cf2303620b7bc69fe458266ea5c7 + languageName: node + linkType: hard + +"@solana/errors@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/errors@npm:2.0.0-preview.2" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.0.0" + bin: + errors: bin/cli.js + checksum: 10c0/5f71ef1306e3c4c858854e5b29750c200c0e49bb9a3e4e2633dd9daa40dc92a940e39e23423b936b110e2bdca092fac546983cde9145a1ad97601ac4d4fdb211 + languageName: node + linkType: hard + +"@solana/errors@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/errors@npm:2.0.0-preview.4" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + peerDependencies: + typescript: ">=5" + bin: + errors: bin/cli.mjs + checksum: 10c0/457a4e35a8a28c1ad7fc75cf1fde6159b920cebce9c6c46af54b001ccecae9c1f366703de9cad238e784a2b76ae180f0b7c9c63d2f7c972d67cd8165960fdae9 + languageName: node + linkType: hard + "@solana/errors@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/errors@npm:2.0.0-rc.1" @@ -3127,6 +3295,79 @@ __metadata: languageName: node linkType: hard +"@solana/options@npm:2.0.0-preview.2": + version: 2.0.0-preview.2 + resolution: "@solana/options@npm:2.0.0-preview.2" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.2" + "@solana/codecs-numbers": "npm:2.0.0-preview.2" + checksum: 10c0/e749161b042145d826d6bc1874e37b2fda725e737dd010c696fe1d7300a93eb48a80c2d2376361cd658ee04b4398194fdfa16e858c39ad737337491a8a71e095 + languageName: node + linkType: hard + +"@solana/options@npm:2.0.0-preview.4": + version: 2.0.0-preview.4 + resolution: "@solana/options@npm:2.0.0-preview.4" + dependencies: + "@solana/codecs-core": "npm:2.0.0-preview.4" + "@solana/codecs-data-structures": "npm:2.0.0-preview.4" + "@solana/codecs-numbers": "npm:2.0.0-preview.4" + "@solana/codecs-strings": "npm:2.0.0-preview.4" + "@solana/errors": "npm:2.0.0-preview.4" + peerDependencies: + typescript: ">=5" + checksum: 10c0/c2b802ee449af09231ad38fff48f21c7dd8bc747415451575c8fded3d91c58cafac61829d3d215d19423e212235ab3ad98efeb96bcfbe3587e3181ccd389cd81 + languageName: node + linkType: hard + +"@solana/spl-token-group@npm:^0.0.5": + version: 0.0.5 + resolution: "@solana/spl-token-group@npm:0.0.5" + dependencies: + "@solana/codecs": "npm:2.0.0-preview.4" + "@solana/spl-type-length-value": "npm:0.1.0" + peerDependencies: + "@solana/web3.js": ^1.94.0 + checksum: 10c0/f7140e21165c24f8ab5d7d74138c5d776b96f1f56f0652e641bb94aac2c4f4ba2f7b48c74fe754f962c27a652ecdf6e0be12491ace99116859330a71454b653b + languageName: node + linkType: hard + +"@solana/spl-token-metadata@npm:^0.1.3": + version: 0.1.4 + resolution: "@solana/spl-token-metadata@npm:0.1.4" + dependencies: + "@solana/codecs": "npm:2.0.0-preview.2" + "@solana/spl-type-length-value": "npm:0.1.0" + peerDependencies: + "@solana/web3.js": ^1.91.6 + checksum: 10c0/d2f27b944e26bff2353c362aee0b61b2d030d4df02332898e96d109c2408721e4c652f30dfe1b77a2f6ec72112137e5c79fcda55ec92405d8ead9aa23159a623 + languageName: node + linkType: hard + +"@solana/spl-token@npm:0.4.8": + version: 0.4.8 + resolution: "@solana/spl-token@npm:0.4.8" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/buffer-layout-utils": "npm:^0.2.0" + "@solana/spl-token-group": "npm:^0.0.5" + "@solana/spl-token-metadata": "npm:^0.1.3" + buffer: "npm:^6.0.3" + peerDependencies: + "@solana/web3.js": ^1.94.0 + checksum: 10c0/6570a439d93544b9822fc67fd94b443a465ddd2be9683937fe67deea6a71b72c04b80da5a895abfbb2e276dac83ae0401c990e7a4fdf7cf01168288b7345da55 + languageName: node + linkType: hard + +"@solana/spl-type-length-value@npm:0.1.0": + version: 0.1.0 + resolution: "@solana/spl-type-length-value@npm:0.1.0" + dependencies: + buffer: "npm:^6.0.3" + checksum: 10c0/a8f2fd6308dffa27827799146857a778ff807380578e187023f8fe90ebf8a68ed1f9f74a0c196cde7b757ea188ff2af040a727c18bb3c86a82f62fe3ec4c43bb + languageName: node + linkType: hard + "@solana/web3.js@npm:1.95.2": version: 1.95.2 resolution: "@solana/web3.js@npm:1.95.2" @@ -3150,6 +3391,29 @@ __metadata: languageName: node linkType: hard +"@solana/web3.js@npm:^1.32.0": + version: 1.95.3 + resolution: "@solana/web3.js@npm:1.95.3" + dependencies: + "@babel/runtime": "npm:^7.25.0" + "@noble/curves": "npm:^1.4.2" + "@noble/hashes": "npm:^1.4.0" + "@solana/buffer-layout": "npm:^4.0.1" + agentkeepalive: "npm:^4.5.0" + bigint-buffer: "npm:^1.1.5" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + bs58: "npm:^4.0.1" + buffer: "npm:6.0.3" + fast-stable-stringify: "npm:^1.0.0" + jayson: "npm:^4.1.1" + node-fetch: "npm:^2.7.0" + rpc-websockets: "npm:^9.0.2" + superstruct: "npm:^2.0.2" + checksum: 10c0/6c3847029924bb0980ce6c1a3ea5cca87e8fc2b694ceb7c897319bf30d9643d57417fc1859aff09b9045adb3905134bd8f063a936b39a551304ee08a7a1f53c9 + languageName: node + linkType: hard + "@substrate/ss58-registry@npm:^1.44.0": version: 1.48.0 resolution: "@substrate/ss58-registry@npm:1.48.0" @@ -4255,6 +4519,7 @@ __metadata: resolution: "@xchainjs/xchain-solana@workspace:packages/xchain-solana" dependencies: "@solana/addresses": "npm:2.0.0-rc.1" + "@solana/spl-token": "npm:0.4.8" "@solana/web3.js": "npm:1.95.2" "@xchainjs/xchain-client": "workspace:*" "@xchainjs/xchain-crypto": "workspace:*" @@ -5054,7 +5319,7 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.1.2": +"bignumber.js@npm:^9.0.1, bignumber.js@npm:^9.1.2": version: 9.1.2 resolution: "bignumber.js@npm:9.1.2" checksum: 10c0/e17786545433f3110b868725c449fa9625366a6e675cd70eb39b60938d6adbd0158cb4b3ad4f306ce817165d37e63f4aa3098ba4110db1d9a3b9f66abfbaf10d @@ -5889,7 +6154,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.1.0": +"commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 From 5f09c76fa1c9ac8c869480aa88e315083e72db1f Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 26 Aug 2024 11:31:25 +0200 Subject: [PATCH 06/21] getBalance method with symbol fix --- packages/xchain-solana/__e2e__/client.ts | 14 +- packages/xchain-solana/package.json | 3 + packages/xchain-solana/src/client.ts | 41 ++++- yarn.lock | 221 ++++++++++++++++++++++- 4 files changed, 259 insertions(+), 20 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index bcbe6f86e..0f4c39541 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -1,4 +1,4 @@ -import { assetToString, baseToAsset } from '@xchainjs/xchain-util' +import { TokenAsset, assetFromStringEx, assetToString, baseToAsset } from '@xchainjs/xchain-util' import { Client, defaultSolanaParams } from '../src' @@ -22,11 +22,21 @@ describe('Solana client', () => { console.log(address) }) - it('Should get address balance', async () => { + it('Should get all address balances', async () => { const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') balances.forEach((balance) => { console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) }) }) + + it('Should get address balance filtering tokens', async () => { + const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v', [ + assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + ]) + + balances.forEach((balance) => { + console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) + }) + }) }) diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json index 92097f75a..2d80044c4 100644 --- a/packages/xchain-solana/package.json +++ b/packages/xchain-solana/package.json @@ -32,6 +32,9 @@ "lint": "eslint \"{src,__tests__}/**/*.ts\" --fix --max-warnings 0" }, "dependencies": { + "@metaplex-foundation/mpl-token-metadata": "3.2.1", + "@metaplex-foundation/umi": "0.9.2", + "@metaplex-foundation/umi-bundle-defaults": "0.9.2", "@solana/addresses": "2.0.0-rc.1", "@solana/spl-token": "0.4.8", "@solana/web3.js": "1.95.2", diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 041c7b4c3..6bd1c9390 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -1,9 +1,11 @@ +import { fetchAllDigitalAsset, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata' +import { PublicKey as UmiPubliKey, Umi, publicKey } from '@metaplex-foundation/umi' +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { isAddress } from '@solana/addresses' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { Connection, Keypair, PublicKey, clusterApiUrl } from '@solana/web3.js' import { AssetInfo, - Balance, BaseXChainClient, ExplorerProviders, Fees, @@ -13,17 +15,18 @@ import { TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' -import { Address, assetFromStringEx, baseAmount } from '@xchainjs/xchain-util' +import { Address, TokenAsset, assetFromStringEx, baseAmount } from '@xchainjs/xchain-util' import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' import { TokenAssetData } from './solana-types' -import { SOLClientParams } from './types' +import { Balance, SOLClientParams } from './types' import { getSolanaNetwork } from './utils' export class Client extends BaseXChainClient { private explorerProviders: ExplorerProviders private connection: Connection + private umi: Umi constructor(params: SOLClientParams = defaultSolanaParams) { super(SOLChain, { @@ -32,6 +35,7 @@ export class Client extends BaseXChainClient { }) this.explorerProviders = params.explorerProviders this.connection = new Connection(clusterApiUrl(getSolanaNetwork(this.getNetwork()))) + this.umi = createUmi(this.connection).use(mplTokenMetadata()) } /** @@ -122,7 +126,7 @@ export class Client extends BaseXChainClient { return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` } - public async getBalance(address: Address): Promise { + public async getBalance(address: Address, assets?: TokenAsset[]): Promise { const balances: Balance[] = [] const nativeBalance = await this.connection.getBalance(new PublicKey(address)) @@ -136,13 +140,32 @@ export class Client extends BaseXChainClient { programId: TOKEN_PROGRAM_ID, }) + const tokensToRequest = !assets + ? tokenBalances.value + : tokenBalances.value.filter((tokenBalance) => { + const tokenData = tokenBalance.account.data.parsed as TokenAssetData + assets.findIndex((asset) => { + return asset.symbol.toLowerCase().includes(tokenData.info.mint.toLowerCase()) + }) !== -1 + }) + + const mintPublicKeys: UmiPubliKey[] = tokensToRequest.map((tokenBalance) => { + const tokenData = tokenBalance.account.data.parsed as TokenAssetData + return publicKey(tokenData.info.mint) + }) + + const assetsData = await fetchAllDigitalAsset(this.umi, mintPublicKeys) + tokenBalances.value.forEach((balance) => { const parsedData = balance.account.data.parsed as TokenAssetData - const symbol = 'TBD' // TODO: Find a way to retrieve symbol data - balances.push({ - amount: baseAmount(parsedData.info.tokenAmount.amount, parsedData.info.tokenAmount.decimals), - asset: assetFromStringEx(`SOL.${symbol}-${parsedData.info.mint}`), - }) + const assetData = assetsData.find((assetData) => assetData.publicKey.toString() === parsedData.info.mint) + + if (assetData) { + balances.push({ + amount: baseAmount(parsedData.info.tokenAmount.amount, parsedData.info.tokenAmount.decimals), + asset: assetFromStringEx(`SOL.${assetData.metadata.symbol.trim()}-${parsedData.info.mint}`) as TokenAsset, + }) + } }) return balances diff --git a/yarn.lock b/yarn.lock index 60eea6f58..3f417a171 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2391,6 +2391,206 @@ __metadata: languageName: node linkType: hard +"@metaplex-foundation/mpl-token-metadata@npm:3.2.1": + version: 3.2.1 + resolution: "@metaplex-foundation/mpl-token-metadata@npm:3.2.1" + dependencies: + "@metaplex-foundation/mpl-toolbox": "npm:^0.9.4" + peerDependencies: + "@metaplex-foundation/umi": ">= 0.8.2 < 1" + checksum: 10c0/6c64e99b12c43362dfad0f809b1754e0bd84680a51a007cf3439a8c5954c3ffb3d6e7130bdf5bcb0304985a8e60cf66cfd9e897ffb8a4f9953eb522b27fac473 + languageName: node + linkType: hard + +"@metaplex-foundation/mpl-toolbox@npm:^0.9.4": + version: 0.9.4 + resolution: "@metaplex-foundation/mpl-toolbox@npm:0.9.4" + peerDependencies: + "@metaplex-foundation/umi": ">= 0.8.2 < 1" + checksum: 10c0/5036b0d941ada428cc199a3d8472bfc18473dd04cc9226c98971aa133e7d1cd619f4c5b25207f1b4470a1f0c5c108f2ef69404c808a5ee3bf4390fa88c47227d + languageName: node + linkType: hard + +"@metaplex-foundation/umi-bundle-defaults@npm:0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-bundle-defaults@npm:0.9.2" + dependencies: + "@metaplex-foundation/umi-downloader-http": "npm:^0.9.2" + "@metaplex-foundation/umi-eddsa-web3js": "npm:^0.9.2" + "@metaplex-foundation/umi-http-fetch": "npm:^0.9.2" + "@metaplex-foundation/umi-program-repository": "npm:^0.9.2" + "@metaplex-foundation/umi-rpc-chunk-get-accounts": "npm:^0.9.2" + "@metaplex-foundation/umi-rpc-web3js": "npm:^0.9.2" + "@metaplex-foundation/umi-serializer-data-view": "npm:^0.9.2" + "@metaplex-foundation/umi-transaction-factory-web3js": "npm:^0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + "@solana/web3.js": ^1.72.0 + checksum: 10c0/a78dabe6c2393199356ddd3c8f221920abba0674472bd71d136e7fde90c76f28cfd29534a531c1773a987691b81a9dc69835d7f22cad091736e9300055156101 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-downloader-http@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-downloader-http@npm:0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + checksum: 10c0/1ae66882f871ca1214d5e42e43391f7fc6f665e2a0e83c3c471f9d5e6328e7ac67d50f9dbe51334ab56309339a20ecce4b4b0a0580d4780a954be5ecd756500c + languageName: node + linkType: hard + +"@metaplex-foundation/umi-eddsa-web3js@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-eddsa-web3js@npm:0.9.2" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": "npm:^0.9.2" + "@noble/curves": "npm:^1.0.0" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + "@solana/web3.js": ^1.72.0 + checksum: 10c0/623fb5d65aa28b394701b9415f2c8d4a4089df4c4d15d19cb2af97fc633c1aaa811696c6a67ef64226eefdbdeb06579812bcf1288bb6731745cb91b4d6ad701f + languageName: node + linkType: hard + +"@metaplex-foundation/umi-http-fetch@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-http-fetch@npm:0.9.2" + dependencies: + node-fetch: "npm:^2.6.7" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + checksum: 10c0/0d608bdeacb07eeac4af607950ff2a6e21fd94a98e6eda6d76d8e7f71b970373cba92cb10bec969f8dcc7c14d561f837e99c7e33b9c83f168624d1f9c37c68ca + languageName: node + linkType: hard + +"@metaplex-foundation/umi-options@npm:^0.8.9": + version: 0.8.9 + resolution: "@metaplex-foundation/umi-options@npm:0.8.9" + checksum: 10c0/da0bcec5188ab5638ee4bbdbba22236b69f5e89f33bf279ef9ea584d5e5ed428ab6a7cddcc73d29b8bf1e8ea840d641b509e01b9778b470baded5ab30d18cc3d + languageName: node + linkType: hard + +"@metaplex-foundation/umi-program-repository@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-program-repository@npm:0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + checksum: 10c0/14ded229854f84b4a30d810171ea35a5576291fa881ae7534dff6b281b1098fd7952ed1616452b37e0f5a73a0c23353d6226ad2f4df5d808dcb7853d25a108b2 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-public-keys@npm:^0.8.9": + version: 0.8.9 + resolution: "@metaplex-foundation/umi-public-keys@npm:0.8.9" + dependencies: + "@metaplex-foundation/umi-serializers-encodings": "npm:^0.8.9" + checksum: 10c0/961281b83cb6b60eb7872fb6c818798ee3b81b5457fb5c31c9b96bc73010d77e12d9952659f7d16d0352eb219ac7644d59d1147f27817d8c46722ea6bad23d83 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-rpc-chunk-get-accounts@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-rpc-chunk-get-accounts@npm:0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + checksum: 10c0/ca86434df4fe1a224f4dcc2b4c7e0b5982f5ab1aa69312e6a35a83f739eb4e914ff09454271a981577ae6d79fd12921a50493835c9d48e4483d3e986400498c7 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-rpc-web3js@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-rpc-web3js@npm:0.9.2" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": "npm:^0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + "@solana/web3.js": ^1.72.0 + checksum: 10c0/55d6b4a3c486e9e980cb5d263e6326c9edf4aafd05da1c2746c5af445140fa0ef354823c48b09a687bbd915323171287ad3b4fd861b7779c7628936fba9b8334 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializer-data-view@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-serializer-data-view@npm:0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + checksum: 10c0/2e1d9ecf65052f8ef3f6c1e0dfa1ea1ced9d98b11b6b1314576a142d012408eed158756f05e277ef09319d25ff6c650cb00713421acc33d691edfcbfdf449795 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-core@npm:^0.8.9": + version: 0.8.9 + resolution: "@metaplex-foundation/umi-serializers-core@npm:0.8.9" + checksum: 10c0/b169b62b55b6fe7f529c94c8387e9bea25c66a7e9c66bc38027fc89d230f267192f0002a16a663a07dff787d3160424c26d090243edda0aefaaf7f0d874b9adb + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-encodings@npm:^0.8.9": + version: 0.8.9 + resolution: "@metaplex-foundation/umi-serializers-encodings@npm:0.8.9" + dependencies: + "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9" + checksum: 10c0/13bd3a58278284d640b941d26580c86e9bc52390dfc6ee145c3934cd50bf65b42838812f2c4d7adfc1aa93be5c5d3293dcabec2d170ed80406941fa252f0a3d9 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-numbers@npm:^0.8.9": + version: 0.8.9 + resolution: "@metaplex-foundation/umi-serializers-numbers@npm:0.8.9" + dependencies: + "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9" + checksum: 10c0/3dcea7c90ab5c97e631a20d1a73f7fec7f5767200187b79ea6c1ee9763f17ae14cb66f401409662e83f406e7c40b980db62c87506711b95c3be67dd52ab1d2bb + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers@npm:^0.9.0": + version: 0.9.0 + resolution: "@metaplex-foundation/umi-serializers@npm:0.9.0" + dependencies: + "@metaplex-foundation/umi-options": "npm:^0.8.9" + "@metaplex-foundation/umi-public-keys": "npm:^0.8.9" + "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9" + "@metaplex-foundation/umi-serializers-encodings": "npm:^0.8.9" + "@metaplex-foundation/umi-serializers-numbers": "npm:^0.8.9" + checksum: 10c0/8f8f93e23666aac4cf1ed8fc5eecb726c9865f526535e379ae599c8b295dd78fe4e325d9efdcd545a85c0f3bdff39896d93b33c8087db962cc10a156b03a29d6 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-transaction-factory-web3js@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-transaction-factory-web3js@npm:0.9.2" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": "npm:^0.9.2" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + "@solana/web3.js": ^1.72.0 + checksum: 10c0/9f6c11a2e68fa4919ba5376d8e27c4c79bb754cb9c375456d774be7caad821a7421d551e1183b52c12610e9d87adcbcbd3e65f3c5a5bf8a535e2ef06b9e874af + languageName: node + linkType: hard + +"@metaplex-foundation/umi-web3js-adapters@npm:^0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi-web3js-adapters@npm:0.9.2" + dependencies: + buffer: "npm:^6.0.3" + peerDependencies: + "@metaplex-foundation/umi": ^0.9.2 + "@solana/web3.js": ^1.72.0 + checksum: 10c0/fa3ea589621411c38f5c7e6114dd6e1eac797644fa1d7288bd40af467fc50e7e46d80317c7f13d5dfdd1d1ac062794cf445c3158e8ee362094ab02f19a493323 + languageName: node + linkType: hard + +"@metaplex-foundation/umi@npm:0.9.2": + version: 0.9.2 + resolution: "@metaplex-foundation/umi@npm:0.9.2" + dependencies: + "@metaplex-foundation/umi-options": "npm:^0.8.9" + "@metaplex-foundation/umi-public-keys": "npm:^0.8.9" + "@metaplex-foundation/umi-serializers": "npm:^0.9.0" + checksum: 10c0/babe76abba8dc8da9e12441dc1624db418b044943e6a4a91c75b6efba58fa6d963940172c5686e25604d8c546eb12024f501856176d23025240de1864cb38b05 + languageName: node + linkType: hard + "@nestjs/axios@npm:0.0.8": version: 0.0.8 resolution: "@nestjs/axios@npm:0.0.8" @@ -2465,21 +2665,21 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.3.0": - version: 1.4.0 - resolution: "@noble/curves@npm:1.4.0" +"@noble/curves@npm:^1.0.0, @noble/curves@npm:^1.4.2": + version: 1.5.0 + resolution: "@noble/curves@npm:1.5.0" dependencies: "@noble/hashes": "npm:1.4.0" - checksum: 10c0/31fbc370df91bcc5a920ca3f2ce69c8cf26dc94775a36124ed8a5a3faf0453badafd2ee4337061ffea1b43c623a90ee8b286a5a81604aaf9563bdad7ff795d18 + checksum: 10c0/89faed98e7ff1fee086777afcf63b7ec237121ebfe09495eb9ff7f73c7dd696000c795a24a1bedadc804b592d4b3c655f2e4a9fe9a3afe312a9e6376558d3737 languageName: node linkType: hard -"@noble/curves@npm:^1.4.2": - version: 1.5.0 - resolution: "@noble/curves@npm:1.5.0" +"@noble/curves@npm:^1.3.0": + version: 1.4.0 + resolution: "@noble/curves@npm:1.4.0" dependencies: "@noble/hashes": "npm:1.4.0" - checksum: 10c0/89faed98e7ff1fee086777afcf63b7ec237121ebfe09495eb9ff7f73c7dd696000c795a24a1bedadc804b592d4b3c655f2e4a9fe9a3afe312a9e6376558d3737 + checksum: 10c0/31fbc370df91bcc5a920ca3f2ce69c8cf26dc94775a36124ed8a5a3faf0453badafd2ee4337061ffea1b43c623a90ee8b286a5a81604aaf9563bdad7ff795d18 languageName: node linkType: hard @@ -4518,6 +4718,9 @@ __metadata: version: 0.0.0-use.local resolution: "@xchainjs/xchain-solana@workspace:packages/xchain-solana" dependencies: + "@metaplex-foundation/mpl-token-metadata": "npm:3.2.1" + "@metaplex-foundation/umi": "npm:0.9.2" + "@metaplex-foundation/umi-bundle-defaults": "npm:0.9.2" "@solana/addresses": "npm:2.0.0-rc.1" "@solana/spl-token": "npm:0.4.8" "@solana/web3.js": "npm:1.95.2" @@ -10228,7 +10431,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.7.0": +"node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From 34fd2b0b42eaeb77f33c38640d91296f5fbe620f Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 26 Aug 2024 18:13:37 +0200 Subject: [PATCH 07/21] Solana native asset estimate fee --- packages/xchain-solana/__e2e__/client.ts | 38 ++++++++++++- packages/xchain-solana/src/client.ts | 69 ++++++++++++++++++++++-- packages/xchain-solana/src/types.ts | 16 +++++- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 0f4c39541..a2ca57946 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -1,4 +1,11 @@ -import { TokenAsset, assetFromStringEx, assetToString, baseToAsset } from '@xchainjs/xchain-util' +import { + TokenAsset, + assetAmount, + assetFromStringEx, + assetToBase, + assetToString, + baseToAsset, +} from '@xchainjs/xchain-util' import { Client, defaultSolanaParams } from '../src' @@ -39,4 +46,33 @@ describe('Solana client', () => { console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) }) }) + + it('Should estimate simple transaction fee', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(), + amount: assetToBase(assetAmount(1, 9)), + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) + + it('Should estimate transaction fee with memo', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(), + amount: assetToBase(assetAmount(1, 9)), + memo: 'Example of memo', + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 6bd1c9390..38f9c31d2 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -3,11 +3,21 @@ import { PublicKey as UmiPubliKey, Umi, publicKey } from '@metaplex-foundation/u import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { isAddress } from '@solana/addresses' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { Connection, Keypair, PublicKey, clusterApiUrl } from '@solana/web3.js' +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, + clusterApiUrl, +} from '@solana/web3.js' import { AssetInfo, BaseXChainClient, ExplorerProviders, + FeeOption, + FeeType, Fees, PreparedTx, Tx, @@ -15,12 +25,12 @@ import { TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' -import { Address, TokenAsset, assetFromStringEx, baseAmount } from '@xchainjs/xchain-util' +import { Address, TokenAsset, assetFromStringEx, baseAmount, eqAsset } from '@xchainjs/xchain-util' import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' import { TokenAssetData } from './solana-types' -import { Balance, SOLClientParams } from './types' +import { Balance, SOLClientParams, TxParams } from './types' import { getSolanaNetwork } from './utils' export class Client extends BaseXChainClient { @@ -126,6 +136,12 @@ export class Client extends BaseXChainClient { return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` } + /** + * Retrieves the balance of a given address. + * @param {Address} address - The address to retrieve the balance for. + * @param {TokenAsset[]} assets - Assets to retrieve the balance for (optional). + * @returns {Promise} An array containing the balance of the address. + */ public async getBalance(address: Address, assets?: TokenAsset[]): Promise { const balances: Balance[] = [] @@ -171,8 +187,51 @@ export class Client extends BaseXChainClient { return balances } - getFees(): Promise { - throw new Error('Method not implemented.') + /** + * + * @param params + * @returns + */ + public async getFees(params?: TxParams): Promise { + if (!params) throw new Error('Params need to be passed') + + const sender = Keypair.generate() + + const transaction = new Transaction() + + transaction.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) + transaction.feePayer = sender.publicKey + + if (!params.asset || eqAsset(params.asset, this.getAssetInfo().asset)) { + transaction.add( + SystemProgram.transfer({ + fromPubkey: sender.publicKey, + toPubkey: new PublicKey(params.recipient), + lamports: params.amount.amount().toNumber(), + }), + ) + } else { + // TODO: Add Token instruction + } + + if (params.memo) { + transaction.add( + new TransactionInstruction({ + keys: [{ pubkey: sender.publicKey, isSigner: true, isWritable: true }], + data: Buffer.from(params.memo, 'utf-8'), + programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), + }), + ) + } + + const fee = await transaction.getEstimatedFee(this.connection) + + return { + type: FeeType.FlatFee, + [FeeOption.Average]: baseAmount(fee || 0, SOL_DECIMALS), + [FeeOption.Fast]: baseAmount(fee || 0, SOL_DECIMALS), + [FeeOption.Fastest]: baseAmount(fee || 0, SOL_DECIMALS), + } } getTransactions(): Promise { diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index 0ccff085f..5495e85c4 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -1,5 +1,11 @@ -import { Balance as BaseBalance, ExplorerProviders, XChainClientParams } from '@xchainjs/xchain-client' +import { + Balance as BaseBalance, + ExplorerProviders, + TxParams as BaseTxParams, + XChainClientParams, +} from '@xchainjs/xchain-client' import { Asset, TokenAsset } from '@xchainjs/xchain-util' + /** * Solana client params */ @@ -7,6 +13,12 @@ export type SOLClientParams = XChainClientParams & { explorerProviders: ExplorerProviders } +export type CompatibleAsset = Asset | TokenAsset + export type Balance = BaseBalance & { - asset: Asset | TokenAsset + asset: CompatibleAsset +} + +export type TxParams = BaseTxParams & { + asset?: CompatibleAsset } From 7332f3aae8a479c5248bbe4bfde0e43d7a528871 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Tue, 27 Aug 2024 18:19:03 +0200 Subject: [PATCH 08/21] getFees --- packages/xchain-solana/__e2e__/client.ts | 65 +++++++++++++++++- packages/xchain-solana/src/client.ts | 87 ++++++++++++++++++++---- packages/xchain-solana/src/types.ts | 4 +- 3 files changed, 139 insertions(+), 17 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index a2ca57946..c7993ddc1 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -4,6 +4,7 @@ import { assetFromStringEx, assetToBase, assetToString, + baseAmount, baseToAsset, } from '@xchainjs/xchain-util' @@ -30,7 +31,7 @@ describe('Solana client', () => { }) it('Should get all address balances', async () => { - const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') + const balances = await client.getBalance(await client.getAddressAsync()) balances.forEach((balance) => { console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) @@ -75,4 +76,66 @@ describe('Solana client', () => { fastest: fees.fastest.amount().toString(), }) }) + + it('Should estimate token transaction fee', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(2), + amount: assetToBase(assetAmount(1, 9)), + asset: assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) + + it('Should estimate transaction fee with priority fee', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(4), + amount: assetToBase(assetAmount(1, 9)), + priorityFee: baseAmount(1000, 9), + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) + + it('Should estimate transaction fee with limit', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(4), + amount: assetToBase(assetAmount(1, 9)), + limit: 30000, + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) + + it('Should estimate transaction with all params', async () => { + const fees = await client.getFees({ + recipient: await client.getAddressAsync(4), + amount: assetToBase(assetAmount(1, 9)), + limit: 30000, + priorityFee: baseAmount(3000000, 9), + memo: 'fsdfsdfsdfds', + }) + + console.log({ + type: fees.type, + average: fees.average.amount().toString(), + fast: fees.fast.amount().toString(), + fastest: fees.fastest.amount().toString(), + }) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 38f9c31d2..928438533 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -2,8 +2,16 @@ import { fetchAllDigitalAsset, mplTokenMetadata } from '@metaplex-foundation/mpl import { PublicKey as UmiPubliKey, Umi, publicKey } from '@metaplex-foundation/umi' import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { isAddress } from '@solana/addresses' -import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { + TOKEN_PROGRAM_ID, + TokenAccountNotFoundError, + TokenInvalidAccountOwnerError, + createAssociatedTokenAccountInstruction, + getAccount, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { + ComputeBudgetProgram, Connection, Keypair, PublicKey, @@ -25,7 +33,14 @@ import { TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' -import { Address, TokenAsset, assetFromStringEx, baseAmount, eqAsset } from '@xchainjs/xchain-util' +import { + Address, + TokenAsset, + assetFromStringEx, + baseAmount, + eqAsset, + getContractAddressFromAsset, +} from '@xchainjs/xchain-util' import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' @@ -97,14 +112,7 @@ export class Client extends BaseXChainClient { * @throws {"Phrase must be provided"} Thrown if the phrase has not been set before. */ public async getAddressAsync(index?: number): Promise { - if (!this.phrase) throw new Error('Phrase must be provided') - - const seed = getSeed(this.phrase) - const hd = HDKey.fromMasterSeed(seed.toString('hex')) - - const keypair = Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index || 0)).privateKey) - - return keypair.publicKey.toBase58() + return this.getPrivateKeyPair(index || 0).publicKey.toBase58() } /** @@ -196,22 +204,46 @@ export class Client extends BaseXChainClient { if (!params) throw new Error('Params need to be passed') const sender = Keypair.generate() + const toPubkey = new PublicKey(params.recipient) const transaction = new Transaction() transaction.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) transaction.feePayer = sender.publicKey + let createAccountTxFee = 0 if (!params.asset || eqAsset(params.asset, this.getAssetInfo().asset)) { + // Native transfer transaction.add( SystemProgram.transfer({ fromPubkey: sender.publicKey, - toPubkey: new PublicKey(params.recipient), + toPubkey, lamports: params.amount.amount().toNumber(), }), ) } else { - // TODO: Add Token instruction + // Token transfer + const mintAddress = new PublicKey(getContractAddressFromAsset(params.asset as TokenAsset)) + const associatedTokenAddress = getAssociatedTokenAddressSync(mintAddress, toPubkey) + + try { + await getAccount(this.connection, associatedTokenAddress, undefined, TOKEN_PROGRAM_ID) + // TODO: Add transfer instruction + } catch (error: unknown) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + // recipient token account has to be created + const createAccountTx = new Transaction() + createAccountTx.add( + createAssociatedTokenAccountInstruction(sender.publicKey, associatedTokenAddress, toPubkey, mintAddress), + ) + createAccountTx.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) + createAccountTx.feePayer = sender.publicKey + + createAccountTxFee = (await createAccountTx.getEstimatedFee(this.connection)) || 0 + } + + // TODO: Add transfer instruction + } } if (params.memo) { @@ -224,13 +256,29 @@ export class Client extends BaseXChainClient { ) } + if (params.priorityFee) { + transaction.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: params.priorityFee.amount().toNumber() / 10 ** 3, + }), + ) + } + + if (params.limit) { + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: params.limit, + }), + ) + } + const fee = await transaction.getEstimatedFee(this.connection) return { type: FeeType.FlatFee, - [FeeOption.Average]: baseAmount(fee || 0, SOL_DECIMALS), - [FeeOption.Fast]: baseAmount(fee || 0, SOL_DECIMALS), - [FeeOption.Fastest]: baseAmount(fee || 0, SOL_DECIMALS), + [FeeOption.Average]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fast]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fastest]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), } } @@ -253,4 +301,13 @@ export class Client extends BaseXChainClient { prepareTx(): Promise { throw new Error('Method not implemented.') } + + private getPrivateKeyPair(index: number): Keypair { + if (!this.phrase) throw new Error('Phrase must be provided') + + const seed = getSeed(this.phrase) + const hd = HDKey.fromMasterSeed(seed.toString('hex')) + + return Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index)).privateKey) + } } diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index 5495e85c4..7705816e6 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -4,7 +4,7 @@ import { TxParams as BaseTxParams, XChainClientParams, } from '@xchainjs/xchain-client' -import { Asset, TokenAsset } from '@xchainjs/xchain-util' +import { Asset, BaseAmount, TokenAsset } from '@xchainjs/xchain-util' /** * Solana client params @@ -21,4 +21,6 @@ export type Balance = BaseBalance & { export type TxParams = BaseTxParams & { asset?: CompatibleAsset + priorityFee?: BaseAmount + limit?: number } From 51d3a224d25110190ccdc4e44c6381ddeaecd0d9 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Wed, 28 Aug 2024 00:42:20 +0200 Subject: [PATCH 09/21] GetFee method with create token account cost estimation --- packages/xchain-solana/src/client.ts | 45 +++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 928438533..f32b04e02 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -6,7 +6,7 @@ import { TOKEN_PROGRAM_ID, TokenAccountNotFoundError, TokenInvalidAccountOwnerError, - createAssociatedTokenAccountInstruction, + createTransferInstruction, getAccount, getAssociatedTokenAddressSync, } from '@solana/spl-token' @@ -196,9 +196,11 @@ export class Client extends BaseXChainClient { } /** + * Get transaction fees. * - * @param params - * @returns + * @param {TxParams} params - The transaction parameters. + * @returns {Fees} The average, fast, and fastest fees. + * @throws {"Params need to be passed"} Thrown if parameters are not provided. */ public async getFees(params?: TxParams): Promise { if (!params) throw new Error('Params need to be passed') @@ -228,21 +230,30 @@ export class Client extends BaseXChainClient { try { await getAccount(this.connection, associatedTokenAddress, undefined, TOKEN_PROGRAM_ID) - // TODO: Add transfer instruction + transaction.add( + createTransferInstruction( + sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key + associatedTokenAddress, + sender.publicKey, + params.amount.amount().toNumber(), + ), + ) } catch (error: unknown) { if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { // recipient token account has to be created - const createAccountTx = new Transaction() - createAccountTx.add( - createAssociatedTokenAccountInstruction(sender.publicKey, associatedTokenAddress, toPubkey, mintAddress), - ) - createAccountTx.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) - createAccountTx.feePayer = sender.publicKey - createAccountTxFee = (await createAccountTx.getEstimatedFee(this.connection)) || 0 - } + const dataLength = 165 // Normally used for Token accounts + createAccountTxFee = await this.connection.getMinimumBalanceForRentExemption(dataLength) - // TODO: Add transfer instruction + transaction.add( + createTransferInstruction( + sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key + toPubkey, // Should be Token account, but as recipient token account should be created, recipient public key + sender.publicKey, + params.amount.amount().toNumber(), + ), + ) + } } } @@ -272,13 +283,13 @@ export class Client extends BaseXChainClient { ) } - const fee = await transaction.getEstimatedFee(this.connection) + const fee = (await transaction.getEstimatedFee(this.connection)) || 0 return { type: FeeType.FlatFee, - [FeeOption.Average]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), - [FeeOption.Fast]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), - [FeeOption.Fastest]: baseAmount((fee || 0) + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Average]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fast]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fastest]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), } } From ca08f3ed5a06d5015386a0765257b0ac233e6f02 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Wed, 28 Aug 2024 12:42:03 +0200 Subject: [PATCH 10/21] Get native transaction data method --- packages/xchain-solana/__e2e__/client.ts | 32 ++++++++++++++- packages/xchain-solana/src/client.ts | 52 +++++++++++++++++++++--- packages/xchain-solana/src/types.ts | 26 ++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index c7993ddc1..717af6a5d 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -8,7 +8,30 @@ import { baseToAsset, } from '@xchainjs/xchain-util' -import { Client, defaultSolanaParams } from '../src' +import { Client, Tx, defaultSolanaParams } from '../src' + +const printTx = (tx: Tx) => { + console.log({ + type: tx.type, + hash: tx.hash, + date: tx.date.toDateString(), + asset: assetToString(tx.asset), + from: tx.from.map((i) => { + return { + from: i.from, + asset: i.asset ? assetToString(i.asset) : 'Unknown', + amount: baseToAsset(i.amount).amount().toString(), + } + }), + to: tx.to.map((o) => { + return { + from: o.to, + asset: o.asset ? assetToString(o.asset) : 'Unknown', + amount: baseToAsset(o.amount).amount().toString(), + } + }), + }) +} describe('Solana client', () => { let client: Client @@ -138,4 +161,11 @@ describe('Solana client', () => { fastest: fees.fastest.amount().toString(), }) }) + + it('Should get native transaction data', async () => { + const tx = await client.getTransactionData( + '34JB9k8JKBvuV4WeePbGNfz8i935d9dSiZGG9zTXx1gVE3fbh8YesQxpUEMXKiTFM4bJtwN48DuNKHBsB51j3ukC', + ) + printTx(tx) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index f32b04e02..c8e3aa804 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -28,8 +28,8 @@ import { FeeType, Fees, PreparedTx, - Tx, TxHash, + TxType, TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' @@ -45,7 +45,7 @@ import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' import { TokenAssetData } from './solana-types' -import { Balance, SOLClientParams, TxParams } from './types' +import { Balance, SOLClientParams, Tx, TxFrom, TxParams, TxTo } from './types' import { getSolanaNetwork } from './utils' export class Client extends BaseXChainClient { @@ -293,11 +293,53 @@ export class Client extends BaseXChainClient { } } - getTransactions(): Promise { - throw new Error('Method not implemented.') + /** + * Get the transaction details of a given transaction ID. + * + * @param {string} txId The transaction ID. + * @returns {Tx} The transaction details. + */ + public async getTransactionData(txId: string): Promise { + const transaction = await this.connection.getParsedTransaction(txId) + if (!transaction) throw Error('Can not find transaction') + + const from: TxFrom[] = [] + const to: TxTo[] = [] + + transaction.transaction.message.accountKeys.forEach((accountKey, index) => { + if (accountKey.writable) { + const preBalance = transaction.meta?.preBalances[index] + const postBalance = transaction.meta?.postBalances[index] + + if (preBalance !== undefined && postBalance !== undefined) { + if (postBalance > preBalance) { + to.push({ + amount: baseAmount(postBalance - preBalance, this.getAssetInfo().decimal), + asset: this.getAssetInfo().asset, + to: accountKey.pubkey.toBase58(), + }) + } else { + from.push({ + amount: baseAmount(preBalance - postBalance, this.getAssetInfo().decimal), + asset: this.getAssetInfo().asset, + from: accountKey.pubkey.toBase58(), + }) + } + } + } + }) + + return { + asset: this.getAssetInfo().asset, + date: new Date((transaction.blockTime || 0) * 1000), + type: TxType.Transfer, + hash: transaction.transaction.signatures[0], + from, + to, + } } - getTransactionData(): Promise { + public async getTransactions(): Promise { throw new Error('Method not implemented.') } diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index 7705816e6..ac41dcaf2 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -1,7 +1,10 @@ import { Balance as BaseBalance, ExplorerProviders, + Tx as BaseTx, + TxFrom as BaseTxFrom, TxParams as BaseTxParams, + TxTo as BaseTxTo, XChainClientParams, } from '@xchainjs/xchain-client' import { Asset, BaseAmount, TokenAsset } from '@xchainjs/xchain-util' @@ -24,3 +27,26 @@ export type TxParams = BaseTxParams & { priorityFee?: BaseAmount limit?: number } + +/** + * Type definition for the sender of a Solana transaction. + */ +export type TxFrom = BaseTxFrom & { + asset?: Asset | TokenAsset +} + +/** + * Type definition for the recipient of a Solana transaction. + */ +export type TxTo = BaseTxTo & { + asset?: Asset | TokenAsset +} + +/** + * Type definition for a Solana transaction. + */ +export type Tx = BaseTx & { + asset: Asset | TokenAsset + from: TxFrom[] + to: TxTo[] +} From 07eabc22e5b649c62e441824df6add43ec7eeda4 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Wed, 28 Aug 2024 22:59:08 +0200 Subject: [PATCH 11/21] Get token transaction data --- packages/xchain-solana/__e2e__/client.ts | 7 ++++ packages/xchain-solana/src/client.ts | 46 ++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 717af6a5d..6a2d868a4 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -168,4 +168,11 @@ describe('Solana client', () => { ) printTx(tx) }) + + it('Should get token transaction data', async () => { + const tx = await client.getTransactionData( + '5gosCpsgg7tDx4d9yCYK4ngfRSPP2jbxe82fKGhUQPJPDjSKn93QjiqKUPWjF1LEbNaDL5RkkjGW7gV8M2PLBMoC', + ) + printTx(tx) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index c8e3aa804..28b0e6a73 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -1,4 +1,4 @@ -import { fetchAllDigitalAsset, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata' +import { fetchAllDigitalAsset, fetchDigitalAsset, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata' import { PublicKey as UmiPubliKey, Umi, publicKey } from '@metaplex-foundation/umi' import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { isAddress } from '@solana/addresses' @@ -36,7 +36,9 @@ import { getSeed } from '@xchainjs/xchain-crypto' import { Address, TokenAsset, + assetAmount, assetFromStringEx, + assetToBase, baseAmount, eqAsset, getContractAddressFromAsset, @@ -318,7 +320,7 @@ export class Client extends BaseXChainClient { asset: this.getAssetInfo().asset, to: accountKey.pubkey.toBase58(), }) - } else { + } else if (preBalance > postBalance) { from.push({ amount: baseAmount(preBalance - postBalance, this.getAssetInfo().decimal), asset: this.getAssetInfo().asset, @@ -329,6 +331,46 @@ export class Client extends BaseXChainClient { } }) + // Tokens transfer + if (transaction.meta?.preTokenBalances && transaction.meta?.postTokenBalances) { + for (let i = 0; i < transaction.meta?.postTokenBalances.length; i++) { + const postBalance = transaction.meta.postTokenBalances[i] + + const preBalance = transaction.meta.preTokenBalances.find( + (preTokenBalance) => preTokenBalance.accountIndex === postBalance.accountIndex, + ) + + const postBalanceAmount = postBalance.uiTokenAmount.uiAmount || 0 + const preBalanceAmount = preBalance?.uiTokenAmount.uiAmount || 0 + + i < transaction.meta.preTokenBalances.length ? transaction.meta.preTokenBalances[i].uiTokenAmount.uiAmount : 0 + + if (preBalance !== null && postBalance !== null) { + const assetDecimals = transaction.meta.postTokenBalances[i].uiTokenAmount.decimals + const mintAddress = transaction.meta.postTokenBalances[i].mint + const owner = transaction.meta.postTokenBalances[i].owner + + const tokenMetadata = await fetchDigitalAsset(this.umi, publicKey(mintAddress)) + + if (owner) { + if (postBalanceAmount > preBalanceAmount) { + to.push({ + amount: assetToBase(assetAmount(postBalanceAmount - preBalanceAmount, assetDecimals)), + asset: assetFromStringEx(`SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`) as TokenAsset, + to: owner, + }) + } else if (preBalanceAmount > postBalanceAmount) { + from.push({ + amount: assetToBase(assetAmount(preBalanceAmount - postBalanceAmount, assetDecimals)), + asset: assetFromStringEx(`SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`) as TokenAsset, + from: owner, + }) + } + } + } + } + } + return { asset: this.getAssetInfo().asset, date: new Date((transaction.blockTime || 0) * 1000), From 7f6f8eeff1facaf01644a44ce74f9af215d04327 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Wed, 28 Aug 2024 23:39:44 +0200 Subject: [PATCH 12/21] Get transactions method --- packages/xchain-solana/__e2e__/client.ts | 5 ++ packages/xchain-solana/src/client.ts | 108 ++++++++++++++--------- packages/xchain-solana/src/types.ts | 5 ++ 3 files changed, 77 insertions(+), 41 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 6a2d868a4..c845cbba5 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -175,4 +175,9 @@ describe('Solana client', () => { ) printTx(tx) }) + + it('Should get transaction history', async () => { + const { txs } = await client.getTransactions({ address: await client.getAddressAsync() }) + txs.forEach((tx: Tx) => printTx(tx)) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 28b0e6a73..2004454bc 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -14,6 +14,7 @@ import { ComputeBudgetProgram, Connection, Keypair, + ParsedTransactionWithMeta, PublicKey, SystemProgram, Transaction, @@ -29,8 +30,8 @@ import { Fees, PreparedTx, TxHash, + TxHistoryParams, TxType, - TxsPage, } from '@xchainjs/xchain-client' import { getSeed } from '@xchainjs/xchain-crypto' import { @@ -47,7 +48,7 @@ import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' import { TokenAssetData } from './solana-types' -import { Balance, SOLClientParams, Tx, TxFrom, TxParams, TxTo } from './types' +import { Balance, SOLClientParams, Tx, TxFrom, TxParams, TxTo, TxsPage } from './types' import { getSolanaNetwork } from './utils' export class Client extends BaseXChainClient { @@ -305,13 +306,65 @@ export class Client extends BaseXChainClient { const transaction = await this.connection.getParsedTransaction(txId) if (!transaction) throw Error('Can not find transaction') + return this.parseTransaction(transaction) + } + + public async getTransactions(params?: TxHistoryParams): Promise { + const signatures = await this.connection.getSignaturesForAddress( + new PublicKey(params?.address || (await this.getAddressAsync())), + ) + + const transactions = await this.connection.getParsedTransactions(signatures.map(({ signature }) => signature)) + + const results = await Promise.allSettled( + transactions + .filter((transaction) => !!transaction) + .map((transaction) => this.parseTransaction(transaction as ParsedTransactionWithMeta)), + ) + + const txs: Tx[] = [] + + results.forEach((result) => { + if (result.status === 'fulfilled') { + txs.push(result.value) + } + }) + + return { + txs, + total: txs.length, + } + } + + transfer(): Promise { + throw new Error('Method not implemented.') + } + + broadcastTx(): Promise { + throw new Error('Method not implemented.') + } + + prepareTx(): Promise { + throw new Error('Method not implemented.') + } + + private getPrivateKeyPair(index: number): Keypair { + if (!this.phrase) throw new Error('Phrase must be provided') + + const seed = getSeed(this.phrase) + const hd = HDKey.fromMasterSeed(seed.toString('hex')) + + return Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index)).privateKey) + } + + private async parseTransaction(tx: ParsedTransactionWithMeta): Promise { const from: TxFrom[] = [] const to: TxTo[] = [] - transaction.transaction.message.accountKeys.forEach((accountKey, index) => { + tx.transaction.message.accountKeys.forEach((accountKey, index) => { if (accountKey.writable) { - const preBalance = transaction.meta?.preBalances[index] - const postBalance = transaction.meta?.postBalances[index] + const preBalance = tx.meta?.preBalances[index] + const postBalance = tx.meta?.postBalances[index] if (preBalance !== undefined && postBalance !== undefined) { if (postBalance > preBalance) { @@ -332,23 +385,21 @@ export class Client extends BaseXChainClient { }) // Tokens transfer - if (transaction.meta?.preTokenBalances && transaction.meta?.postTokenBalances) { - for (let i = 0; i < transaction.meta?.postTokenBalances.length; i++) { - const postBalance = transaction.meta.postTokenBalances[i] + if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) { + for (let i = 0; i < tx.meta?.postTokenBalances.length; i++) { + const postBalance = tx.meta.postTokenBalances[i] - const preBalance = transaction.meta.preTokenBalances.find( + const preBalance = tx.meta.preTokenBalances.find( (preTokenBalance) => preTokenBalance.accountIndex === postBalance.accountIndex, ) const postBalanceAmount = postBalance.uiTokenAmount.uiAmount || 0 const preBalanceAmount = preBalance?.uiTokenAmount.uiAmount || 0 - i < transaction.meta.preTokenBalances.length ? transaction.meta.preTokenBalances[i].uiTokenAmount.uiAmount : 0 - if (preBalance !== null && postBalance !== null) { - const assetDecimals = transaction.meta.postTokenBalances[i].uiTokenAmount.decimals - const mintAddress = transaction.meta.postTokenBalances[i].mint - const owner = transaction.meta.postTokenBalances[i].owner + const assetDecimals = tx.meta.postTokenBalances[i].uiTokenAmount.decimals + const mintAddress = tx.meta.postTokenBalances[i].mint + const owner = tx.meta.postTokenBalances[i].owner const tokenMetadata = await fetchDigitalAsset(this.umi, publicKey(mintAddress)) @@ -373,36 +424,11 @@ export class Client extends BaseXChainClient { return { asset: this.getAssetInfo().asset, - date: new Date((transaction.blockTime || 0) * 1000), + date: new Date((tx.blockTime || 0) * 1000), type: TxType.Transfer, - hash: transaction.transaction.signatures[0], + hash: tx.transaction.signatures[0], from, to, } } - - public async getTransactions(): Promise { - throw new Error('Method not implemented.') - } - - transfer(): Promise { - throw new Error('Method not implemented.') - } - - broadcastTx(): Promise { - throw new Error('Method not implemented.') - } - - prepareTx(): Promise { - throw new Error('Method not implemented.') - } - - private getPrivateKeyPair(index: number): Keypair { - if (!this.phrase) throw new Error('Phrase must be provided') - - const seed = getSeed(this.phrase) - const hd = HDKey.fromMasterSeed(seed.toString('hex')) - - return Keypair.fromSeed(hd.derive(this.getFullDerivationPath(index)).privateKey) - } } diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index ac41dcaf2..998fab231 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -5,6 +5,7 @@ import { TxFrom as BaseTxFrom, TxParams as BaseTxParams, TxTo as BaseTxTo, + TxsPage as BaseTxsPage, XChainClientParams, } from '@xchainjs/xchain-client' import { Asset, BaseAmount, TokenAsset } from '@xchainjs/xchain-util' @@ -50,3 +51,7 @@ export type Tx = BaseTx & { from: TxFrom[] to: TxTo[] } + +export type TxsPage = BaseTxsPage & { + txs: Tx[] +} From 23ec4b57795875ea04dda02efccb4c585f82e8cb Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Thu, 29 Aug 2024 17:33:59 +0200 Subject: [PATCH 13/21] Native asset transfer --- packages/xchain-solana/__e2e__/client.ts | 19 ++++ packages/xchain-solana/package.json | 1 + packages/xchain-solana/src/client.ts | 117 +++++++++++++++++++++-- yarn.lock | 17 ++++ 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index c845cbba5..1f87d65e6 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -180,4 +180,23 @@ describe('Solana client', () => { const { txs } = await client.getTransactions({ address: await client.getAddressAsync() }) txs.forEach((tx: Tx) => printTx(tx)) }) + + it('Should prepare native transaction', async () => { + const { rawUnsignedTx } = await client.prepareTx({ + sender: await client.getAddressAsync(0), + recipient: await client.getAddressAsync(1), + amount: assetToBase(assetAmount(0.005, 9)), + }) + + console.log(rawUnsignedTx) + }) + + it('Should send native transaction', async () => { + const hash = await client.transfer({ + recipient: await client.getAddressAsync(1), + amount: assetToBase(assetAmount(0.005, 9)), + }) + + console.log(hash) + }) }) diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json index 2d80044c4..d160141a1 100644 --- a/packages/xchain-solana/package.json +++ b/packages/xchain-solana/package.json @@ -41,6 +41,7 @@ "@xchainjs/xchain-client": "workspace:*", "@xchainjs/xchain-crypto": "workspace:*", "@xchainjs/xchain-util": "workspace:*", + "bs58": "6.0.0", "micro-ed25519-hdkey": "0.1.2" }, "publishConfig": { diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 2004454bc..3b1a932f7 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -16,6 +16,7 @@ import { Keypair, ParsedTransactionWithMeta, PublicKey, + SendTransactionError, SystemProgram, Transaction, TransactionInstruction, @@ -44,6 +45,7 @@ import { eqAsset, getContractAddressFromAsset, } from '@xchainjs/xchain-util' +import bs58 from 'bs58' import { HDKey } from 'micro-ed25519-hdkey' import { SOLAsset, SOLChain, SOL_DECIMALS, defaultSolanaParams } from './const' @@ -336,16 +338,119 @@ export class Client extends BaseXChainClient { } } - transfer(): Promise { - throw new Error('Method not implemented.') + /** + * Transfers SOL or Solana token + * + * @param {TxParams} params The transfer options. + * @returns {TxHash} The transaction hash. + */ + public async transfer({ + walletIndex, + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }: TxParams): Promise { + const senderKeyPair = this.getPrivateKeyPair(walletIndex || 0) + const { rawUnsignedTx } = await this.prepareTx({ + sender: senderKeyPair.publicKey.toBase58(), + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }) + + const transaction = Transaction.from(bs58.decode(rawUnsignedTx)) + + transaction.sign(senderKeyPair) + + return this.broadcastTx(bs58.encode(transaction.serialize())) } - broadcastTx(): Promise { - throw new Error('Method not implemented.') + /** + * Broadcast a transaction to the network + * @param {string} txHex Raw transaction to broadcast + * @returns {TxHash} The hash of the transaction broadcasted + */ + public async broadcastTx(txHex: string): Promise { + try { + const transaction = Transaction.from(bs58.decode(txHex)) + return await this.connection.sendRawTransaction(transaction.serialize()) + } catch (e: unknown) { + if (e instanceof SendTransactionError) { + console.log(await e.getLogs(this.connection)) + } + throw Error('Can not broadcast transaction. Unknown error') + } } - prepareTx(): Promise { - throw new Error('Method not implemented.') + /** + * Prepares a transaction for transfer. + * + * @param {TxParams&Address} params - The transfer options. + * @returns {Promise} The raw unsigned transaction. + */ + public async prepareTx({ + sender, + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }: TxParams & { sender: Address }): Promise { + const transaction = new Transaction() + + const fromPubkey = new PublicKey(sender) + const toPubkey = new PublicKey(recipient) + + transaction.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) + transaction.feePayer = new PublicKey(sender) + + if (!asset || eqAsset(asset, this.getAssetInfo().asset)) { + // Native transfer + transaction.add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports: amount.amount().toNumber(), + }), + ) + } else { + // Token transfer + } + + if (memo) { + transaction.add( + new TransactionInstruction({ + keys: [{ pubkey: fromPubkey, isSigner: true, isWritable: true }], + data: Buffer.from(memo, 'utf-8'), + programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), + }), + ) + } + + if (priorityFee) { + transaction.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: priorityFee.amount().toNumber() / 10 ** 3, + }), + ) + } + + if (limit) { + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: limit, + }), + ) + } + + return { rawUnsignedTx: bs58.encode(transaction.serialize({ verifySignatures: false })) } } private getPrivateKeyPair(index: number): Keypair { diff --git a/yarn.lock b/yarn.lock index 3f417a171..3cd6820c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4727,6 +4727,7 @@ __metadata: "@xchainjs/xchain-client": "workspace:*" "@xchainjs/xchain-crypto": "workspace:*" "@xchainjs/xchain-util": "workspace:*" + bs58: "npm:6.0.0" micro-ed25519-hdkey: "npm:0.1.2" languageName: unknown linkType: soft @@ -5407,6 +5408,13 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^5.0.0": + version: 5.0.0 + resolution: "base-x@npm:5.0.0" + checksum: 10c0/8787a582737a77f7c3d14b92de4812843af99fc62da8792f961e13c56958dc545e9ddab55f726d63987dded9eb732dc7de465730fa3db159c0de14a95067c74a + languageName: node + linkType: hard + "base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -5829,6 +5837,15 @@ __metadata: languageName: node linkType: hard +"bs58@npm:6.0.0": + version: 6.0.0 + resolution: "bs58@npm:6.0.0" + dependencies: + base-x: "npm:^5.0.0" + checksum: 10c0/61910839746625ee4f69369f80e2634e2123726caaa1da6b3bcefcf7efcd9bdca86603360fed9664ffdabe0038c51e542c02581c72ca8d44f60329fe1a6bc8f4 + languageName: node + linkType: hard + "bs58@npm:=4.0.1, bs58@npm:^4.0.0, bs58@npm:^4.0.1": version: 4.0.1 resolution: "bs58@npm:4.0.1" From 44dc1f14f3be6d6dd4a4e84ce931f6995683c3b3 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Fri, 30 Aug 2024 10:36:37 +0200 Subject: [PATCH 14/21] Solana token transfer --- packages/xchain-solana/__e2e__/client.ts | 47 ++++++++++++++++++++++++ packages/xchain-solana/src/client.ts | 41 +++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index 1f87d65e6..ea4ebe80d 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -53,6 +53,11 @@ describe('Solana client', () => { console.log(address) }) + it('Should get address with index 2', async () => { + const address = await client.getAddressAsync(2) + console.log(address) + }) + it('Should get all address balances', async () => { const balances = await client.getBalance(await client.getAddressAsync()) @@ -199,4 +204,46 @@ describe('Solana client', () => { console.log(hash) }) + + it('Should prepare token transaction', async () => { + const { rawUnsignedTx } = await client.prepareTx({ + sender: await client.getAddressAsync(0), + recipient: await client.getAddressAsync(0), + amount: assetToBase(assetAmount(1, 6)), + asset: assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + }) + + console.log(rawUnsignedTx) + }) + + it('Should not prepare token transaction', async () => { + const { rawUnsignedTx } = await client.prepareTx({ + sender: await client.getAddressAsync(0), + recipient: await client.getAddressAsync(2), // Or address with token account created + amount: assetToBase(assetAmount(1, 6)), + asset: assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + }) + + console.log(rawUnsignedTx) + }) + + it('Should do token transaction with no Token account creation transfer', async () => { + const hash = await client.transfer({ + recipient: await client.getAddressAsync(1), + amount: assetToBase(assetAmount(1, 6)), + asset: assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + }) + + console.log(hash) + }) + + it('Should do token transaction with Token account creation transfer', async () => { + const hash = await client.transfer({ + recipient: await client.getAddressAsync(2), + amount: assetToBase(assetAmount(1, 6)), + asset: assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + }) + + console.log(hash) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 3b1a932f7..628bedc01 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -3,12 +3,14 @@ import { PublicKey as UmiPubliKey, Umi, publicKey } from '@metaplex-foundation/u import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { isAddress } from '@solana/addresses' import { + Account, TOKEN_PROGRAM_ID, TokenAccountNotFoundError, TokenInvalidAccountOwnerError, createTransferInstruction, getAccount, getAssociatedTokenAddressSync, + getOrCreateAssociatedTokenAccount, } from '@solana/spl-token' import { ComputeBudgetProgram, @@ -354,6 +356,13 @@ export class Client extends BaseXChainClient { priorityFee, }: TxParams): Promise { const senderKeyPair = this.getPrivateKeyPair(walletIndex || 0) + + if (asset && !eqAsset(asset, this.getAssetInfo().asset)) { + // Check if receipt token account is created, otherwise, create it + const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) + await getOrCreateAssociatedTokenAccount(this.connection, senderKeyPair, mintAddress, new PublicKey(recipient)) + } + const { rawUnsignedTx } = await this.prepareTx({ sender: senderKeyPair.publicKey.toBase58(), recipient, @@ -422,6 +431,38 @@ export class Client extends BaseXChainClient { ) } else { // Token transfer + const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) + + const fromAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, fromPubkey) + let fromTokenAccount: Account + try { + fromTokenAccount = await getAccount(this.connection, fromAssociatedAccount) + } catch (error) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + throw Error('Can not find sender Token account') + } + throw error + } + + const toAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, toPubkey) + let toTokenAccount: Account + try { + toTokenAccount = await getAccount(this.connection, toAssociatedAccount) + } catch (error) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + throw Error('Can not find recipient Token account. Create it first') + } + throw error + } + + transaction.add( + createTransferInstruction( + fromTokenAccount.address, + toTokenAccount.address, + fromPubkey, + amount.amount().toNumber(), + ), + ) } if (memo) { From 815cd05e3927014854936b03868d42c1857406e8 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Fri, 30 Aug 2024 12:50:32 +0200 Subject: [PATCH 15/21] Solana readme --- README.md | 1 + packages/xchain-solana/README.md | 73 +++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ce006f6be..973c003fe 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Blockchain clients with whom you can prepare, make and broadcast transactions, e | [@xchainjs/xchain-bsc](https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-bsc) | ✅ | ✅ | [![npm](https://img.shields.io/npm/v/@xchainjs/xchain-bsc)](https://www.npmjs.com/package/@xchainjs/xchain-bsc) | | [@xchainjs/xchain-kujira](https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-kujira) | ✅ | ❌ | [![npm](https://img.shields.io/npm/v/@xchainjs/xchain-kujira)](https://www.npmjs.com/package/@xchainjs/xchain-kujira) | | [@xchainjs/xchain-cosmos](https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-cosmos) | ✅ | ✅ | [![npm](https://img.shields.io/npm/v/@xchainjs/xchain-cosmos)](https://www.npmjs.com/package/@xchainjs/xchain-cosmos) | +| [@xchainjs/xchain-solana](https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-solana) | ✅ | ❌ | [![npm](https://img.shields.io/npm/v/@xchainjs/xchain-solana)](https://www.npmjs.com/package/@xchainjs/xchain-solana) | | [@xchainjs/xchain-binance](https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-binance) | ✅ | ❌ | [![npm](https://img.shields.io/npm/v/@xchainjs/xchain-binance)](https://www.npmjs.com/package/@xchainjs/xchain-binance) | diff --git a/packages/xchain-solana/README.md b/packages/xchain-solana/README.md index b1dac65b9..25ed780cb 100644 --- a/packages/xchain-solana/README.md +++ b/packages/xchain-solana/README.md @@ -1,14 +1,75 @@
-

Solana

+

Solana client

- - NPM Version + + NPM Version - - NPM Downloads + + NPM Downloads

-
\ No newline at end of file +
+ +Client that allows to perform operations on the Solana blockchain abstracting developers from its particularities, thus allowing developers to focus on their projects. The Solana client is built on top of [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) and the suite of packages developed by the [Metaplex](https://www.metaplex.com/) foundation. + +If you want to read more about Solana blockchain, go to its official [web site](https://solana.com/) + + +## Installation + +```sh +yarn add @xchainjs/xchain-solana +``` +or + +```sh +npm install @xchainjs/xchain-solana +``` + +## Initialization + +Using the Solana client you can initialize the main class of the module in consultation mode if you do not provide any parameters, this means you could retrieve information from the blockchain and prepare transactions to sign, but you will not be able to sign transactions, or generate addresses. + +```ts +import { Client } from '@xchainjs/xchain-solana' + +const client = new Client() + +// Make read operations with your client +``` + +Otherwise, if you want to sign transactions and get the addresses you own, you will need to initialize the main class of the protocol as follows + +```ts +import { Client, defaultSolanaParams } from '@xchainjs/xchain-solana' + +const client = new Client({ + phrase: 'your secret phrase', + ...defaultSolanaParams +}) + +// Make read or write operations with your client +``` + +## Features + +Thanks to the Solana client you will be able to: +- Get the Solana and tokens balances that an address owns +- Generate addresses given a secret phrase +- Transfer Solana and tokens to another address +- Get details of a transaction +- Get address transaction history + + + +## Examples + +You can find examples using the Solana package in the [solana](https://github.com/xchainjs/xchainjs-lib/tree/master/examples/solana) examples folder. + + +## Documentation + +More information about how to use the Solana client can be found on [documentation](https://xchainjs.gitbook.io/xchainjs/clients/xchain-solana) From d567f066f31c42549d481054725d617522d76a26 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Fri, 30 Aug 2024 18:02:50 +0200 Subject: [PATCH 16/21] Get balances bug fix --- packages/xchain-solana/src/client.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 628bedc01..757bbdbd4 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -175,9 +175,11 @@ export class Client extends BaseXChainClient { ? tokenBalances.value : tokenBalances.value.filter((tokenBalance) => { const tokenData = tokenBalance.account.data.parsed as TokenAssetData - assets.findIndex((asset) => { - return asset.symbol.toLowerCase().includes(tokenData.info.mint.toLowerCase()) - }) !== -1 + return ( + assets.findIndex((asset) => { + return asset.symbol.toLowerCase().includes(tokenData.info.mint.toLowerCase()) + }) !== -1 + ) }) const mintPublicKeys: UmiPubliKey[] = tokensToRequest.map((tokenBalance) => { From 41e6545bcdf25b78f468317bf12fc98d367ae02e Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Fri, 30 Aug 2024 18:40:44 +0200 Subject: [PATCH 17/21] Solana examples --- examples/solana/.codesandbox/tasks.json | 10 ++++ .../solana/.devcontainer/devcontainer.json | 5 ++ examples/solana/CHANGELOG.md | 2 + examples/solana/README.md | 59 +++++++++++++++++++ examples/solana/address.ts | 15 +++++ examples/solana/balance-all.ts | 22 +++++++ examples/solana/balance-token.ts | 23 ++++++++ examples/solana/package.json | 26 ++++++++ examples/solana/transaction-prepare.ts | 24 ++++++++ examples/solana/transaction-transfer-token.ts | 29 +++++++++ examples/solana/transaction-transfer.ts | 27 +++++++++ examples/solana/tsconfig.json | 10 ++++ yarn.lock | 14 ++++- 13 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 examples/solana/.codesandbox/tasks.json create mode 100644 examples/solana/.devcontainer/devcontainer.json create mode 100644 examples/solana/CHANGELOG.md create mode 100644 examples/solana/README.md create mode 100644 examples/solana/address.ts create mode 100644 examples/solana/balance-all.ts create mode 100644 examples/solana/balance-token.ts create mode 100644 examples/solana/package.json create mode 100644 examples/solana/transaction-prepare.ts create mode 100644 examples/solana/transaction-transfer-token.ts create mode 100644 examples/solana/transaction-transfer.ts create mode 100644 examples/solana/tsconfig.json diff --git a/examples/solana/.codesandbox/tasks.json b/examples/solana/.codesandbox/tasks.json new file mode 100644 index 000000000..0428d7f95 --- /dev/null +++ b/examples/solana/.codesandbox/tasks.json @@ -0,0 +1,10 @@ +{ + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [ + { + "name": "Install Dependencies", + "command": "yarn install" + } + ] +} + \ No newline at end of file diff --git a/examples/solana/.devcontainer/devcontainer.json b/examples/solana/.devcontainer/devcontainer.json new file mode 100644 index 000000000..10d754993 --- /dev/null +++ b/examples/solana/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "name": "Devcontainer", + "image": "ghcr.io/codesandbox/devcontainers/typescript-node:latest" +} + \ No newline at end of file diff --git a/examples/solana/CHANGELOG.md b/examples/solana/CHANGELOG.md new file mode 100644 index 000000000..e538ac4e0 --- /dev/null +++ b/examples/solana/CHANGELOG.md @@ -0,0 +1,2 @@ +# xchainjs-solana + diff --git a/examples/solana/README.md b/examples/solana/README.md new file mode 100644 index 000000000..194129678 --- /dev/null +++ b/examples/solana/README.md @@ -0,0 +1,59 @@ +# Solana + +Solana examples to show different use cases using the its client + +## Examples + +### Balances + +#### Get all balances + +Check out how you should get all balances an address owns in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/balances-all.ts) or run it as + +```sh +yarn allBalances address +``` + +#### Get specific asset balance + +Check out how you should get specific token balances an address owns in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/balance.ts) or run it as + +```sh +yarn tokenBalance address token +``` + +### Addresses + +#### Get address by index + +Check out how you should get you account address at certain index in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/address.ts) or run it as + +```sh +yarn address phrase index +``` + +### Transactions + +#### Prepare transaction + +Check out how you should prepare a transaction to be signed in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/address.ts) or run it as + +```sh +yarn prepareTx sender recipient asset assetDecimals amount +``` + +#### Make transaction + +Check out how you should make a Solana native asset transaction in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/address.ts) or run it as + +```sh +yarn transfer phrase recipient amount +``` + +#### Make token transaction + +Check out how you should make a Solana token transaction in this [example](https://github.com/xchainjs/xchainjs-lib/blob/master/examples/solana/address.ts) or run it as + +```sh +yarn transferToken phrase recipient asset assetDecimals amount +``` \ No newline at end of file diff --git a/examples/solana/address.ts b/examples/solana/address.ts new file mode 100644 index 000000000..5cf87ee3f --- /dev/null +++ b/examples/solana/address.ts @@ -0,0 +1,15 @@ +import { Client, defaultSolanaParams } from '@xchainjs/xchain-solana' + +const main = async () => { + const phrase = `${process.argv[2]}` + const index = process.argv[3] ? Number(process.argv[3]) : 0 + const client = new Client({ ...defaultSolanaParams, phrase }) + + const address = await client.getAddressAsync(index) + + console.log(`You account at index ${index} is ${address}`) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/balance-all.ts b/examples/solana/balance-all.ts new file mode 100644 index 000000000..7bddc5cb5 --- /dev/null +++ b/examples/solana/balance-all.ts @@ -0,0 +1,22 @@ +import { Client } from '@xchainjs/xchain-solana' +import { assetToString, baseToAsset } from '@xchainjs/xchain-util' + +const main = async () => { + const address = `${process.argv[2]}` + const client = new Client() + + const balances = await client.getBalance(address) + + console.log('---------------------------------------------------') + console.log(`${address} balances`) + console.log('---------------------------------------------------') + console.table( + balances.map((balance) => { + return { Asset: assetToString(balance.asset), Amount: baseToAsset(balance.amount).amount().toString() } + }), + ) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/balance-token.ts b/examples/solana/balance-token.ts new file mode 100644 index 000000000..22f86e5ca --- /dev/null +++ b/examples/solana/balance-token.ts @@ -0,0 +1,23 @@ +import { Client } from '@xchainjs/xchain-solana' +import { TokenAsset, assetFromStringEx, assetToString, baseToAsset } from '@xchainjs/xchain-util' + +const main = async () => { + const address = `${process.argv[2]}` + const asset = assetFromStringEx(`${process.argv[3]}`) as TokenAsset + const client = new Client() + + const balances = await client.getBalance(address, [asset]) + + console.log('---------------------------------------------------') + console.log(`${address} balances`) + console.log('---------------------------------------------------') + console.table( + balances.map((balance) => { + return { Asset: assetToString(balance.asset), Amount: baseToAsset(balance.amount).amount().toString() } + }), + ) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/package.json b/examples/solana/package.json new file mode 100644 index 000000000..7e05fce5e --- /dev/null +++ b/examples/solana/package.json @@ -0,0 +1,26 @@ +{ + "name": "xchainjs-solana", + "private": true, + "version": "0.0.1", + "scripts": { + "allBalances": "npx ts-node balance-all.ts", + "tokenBalance": "npx ts-node balance-token.ts", + "address": "npx ts-node address.ts", + "prepareTx": "npx ts-node transaction-prepare.ts", + "transfer": "npx ts-node transaction-transfer.ts", + "transferToken": "npx ts-node transaction-transfer-token.ts", + "build": "tsc --noEmit" + }, + "description": "Examples using Solana client", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@xchainjs/xchain-solana": "workspace:*", + "@xchainjs/xchain-util": "workspace:*" + }, + "devDependencies": { + "@types/node": "20.11.28", + "ts-node": "10.9.2", + "typescript": "^5.0.4" + } +} \ No newline at end of file diff --git a/examples/solana/transaction-prepare.ts b/examples/solana/transaction-prepare.ts new file mode 100644 index 000000000..0f424598b --- /dev/null +++ b/examples/solana/transaction-prepare.ts @@ -0,0 +1,24 @@ +import { Client } from '@xchainjs/xchain-solana' +import { Asset, TokenAsset, assetAmount, assetFromStringEx, assetToBase } from '@xchainjs/xchain-util' + +const main = async () => { + const sender = `${process.argv[2]}` + const recipient = `${process.argv[3]}` + const asset = assetFromStringEx(`${process.argv[4]}`) as Asset | TokenAsset + const amount = assetAmount(`${process.argv[6]}`, Number(process.argv[5])) + + const client = new Client() + + const { rawUnsignedTx } = await client.prepareTx({ + sender, + recipient, + asset, + amount: assetToBase(amount), + }) + + console.log(rawUnsignedTx) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/transaction-transfer-token.ts b/examples/solana/transaction-transfer-token.ts new file mode 100644 index 000000000..10602e2fb --- /dev/null +++ b/examples/solana/transaction-transfer-token.ts @@ -0,0 +1,29 @@ +import { Client, defaultSolanaParams } from '@xchainjs/xchain-solana' +import { Asset, TokenAsset, assetAmount, assetFromStringEx, assetToBase } from '@xchainjs/xchain-util' + +const main = async () => { + const phrase = `${process.argv[2]}` + const recipient = `${process.argv[3]}` + const asset = assetFromStringEx(`${process.argv[4]}`) as Asset | TokenAsset + const amount = assetAmount(`${process.argv[6]}`, Number(process.argv[5])) + + const client = new Client({ + ...defaultSolanaParams, + phrase, + }) + + const hash = await client.transfer({ + recipient, + asset, + amount: assetToBase(amount), + }) + + console.log({ + hash, + url: client.getExplorerTxUrl(hash), + }) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/transaction-transfer.ts b/examples/solana/transaction-transfer.ts new file mode 100644 index 000000000..6a794d0b9 --- /dev/null +++ b/examples/solana/transaction-transfer.ts @@ -0,0 +1,27 @@ +import { Client, SOL_DECIMALS, defaultSolanaParams } from '@xchainjs/xchain-solana' +import { assetAmount, assetToBase } from '@xchainjs/xchain-util' + +const main = async () => { + const phrase = `${process.argv[2]}` + const recipient = `${process.argv[3]}` + const amount = assetAmount(`${process.argv[4]}`, SOL_DECIMALS) + + const client = new Client({ + ...defaultSolanaParams, + phrase, + }) + + const hash = await client.transfer({ + recipient, + amount: assetToBase(amount), + }) + + console.log({ + hash, + url: client.getExplorerTxUrl(hash), + }) +} + +main() + .then(() => process.exit(0)) + .catch((err) => console.error(err)) diff --git a/examples/solana/tsconfig.json b/examples/solana/tsconfig.json new file mode 100644 index 000000000..f6dc52d0e --- /dev/null +++ b/examples/solana/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module":"commonjs", + "target": "es5", + "noEmitOnError": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["es6", "dom", "es2016", "es2017"] + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3cd6820c1..614670e41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4714,7 +4714,7 @@ __metadata: languageName: unknown linkType: soft -"@xchainjs/xchain-solana@workspace:packages/xchain-solana": +"@xchainjs/xchain-solana@workspace:*, @xchainjs/xchain-solana@workspace:packages/xchain-solana": version: 0.0.0-use.local resolution: "@xchainjs/xchain-solana@workspace:packages/xchain-solana" dependencies: @@ -13815,6 +13815,18 @@ __metadata: languageName: unknown linkType: soft +"xchainjs-solana@workspace:examples/solana": + version: 0.0.0-use.local + resolution: "xchainjs-solana@workspace:examples/solana" + dependencies: + "@types/node": "npm:20.11.28" + "@xchainjs/xchain-solana": "workspace:*" + "@xchainjs/xchain-util": "workspace:*" + ts-node: "npm:10.9.2" + typescript: "npm:^5.0.4" + languageName: unknown + linkType: soft + "xchainjs-wallet@workspace:examples/wallet": version: 0.0.0-use.local resolution: "xchainjs-wallet@workspace:examples/wallet" From 2b0b94f32dd1c7d31a30efb235eb2ac90893c694 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 2 Sep 2024 12:56:02 +0200 Subject: [PATCH 18/21] Tests --- packages/xchain-solana/__e2e__/client.ts | 4 +- .../mpl-token-metadata.ts | 243 +++++++++++ .../umi-bundle-defaults.ts | 17 + .../__mocks__/@solana/web3.js.ts | 387 ++++++++++++++++++ .../xchain-solana/__tests__/client.test.ts | 122 +++++- packages/xchain-solana/src/client.ts | 26 +- 6 files changed, 778 insertions(+), 21 deletions(-) create mode 100644 packages/xchain-solana/__mocks__/@metaplex-foundation/mpl-token-metadata.ts create mode 100644 packages/xchain-solana/__mocks__/@metaplex-foundation/umi-bundle-defaults.ts create mode 100644 packages/xchain-solana/__mocks__/@solana/web3.js.ts diff --git a/packages/xchain-solana/__e2e__/client.ts b/packages/xchain-solana/__e2e__/client.ts index ea4ebe80d..af6147156 100644 --- a/packages/xchain-solana/__e2e__/client.ts +++ b/packages/xchain-solana/__e2e__/client.ts @@ -10,6 +10,8 @@ import { import { Client, Tx, defaultSolanaParams } from '../src' +jest.deepUnmock('@solana/web3.js') + const printTx = (tx: Tx) => { console.log({ type: tx.type, @@ -59,7 +61,7 @@ describe('Solana client', () => { }) it('Should get all address balances', async () => { - const balances = await client.getBalance(await client.getAddressAsync()) + const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') balances.forEach((balance) => { console.log(`${assetToString(balance.asset)}: ${baseToAsset(balance.amount).amount().toString()}`) diff --git a/packages/xchain-solana/__mocks__/@metaplex-foundation/mpl-token-metadata.ts b/packages/xchain-solana/__mocks__/@metaplex-foundation/mpl-token-metadata.ts new file mode 100644 index 000000000..c81fde990 --- /dev/null +++ b/packages/xchain-solana/__mocks__/@metaplex-foundation/mpl-token-metadata.ts @@ -0,0 +1,243 @@ +// import { Context, PublicKey, RpcGetAccountsOptions } from '@metaplex-foundation/umi' + +const mplTtokenMetadataRealPackage = jest.requireActual('@metaplex-foundation/mpl-token-metadata') + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const fetchAllDigitalAsset = async (_umi: any, pks: string[]) => { + return [ + { + publicKey: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + mint: { + publicKey: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + header: { + executable: false, + owner: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + lamports: { + basisPoints: 87661392404, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + mintAuthority: { + __option: 'Some', + value: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + }, + supply: 1889938175280062, + decimals: 6, + isInitialized: true, + freezeAuthority: { + __option: 'Some', + value: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + }, + }, + metadata: { + publicKey: '8c3zk1t1qt3RU43ckuvPkCS7HLbjJqq3J3Me8ov4aHrp', + header: { + executable: false, + owner: 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', + lamports: { + basisPoints: 5616720, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + key: 4, + updateAuthority: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + name: 'USDT', + symbol: 'USDT', + uri: '', + sellerFeeBasisPoints: 0, + creators: { + __option: 'None', + }, + primarySaleHappened: false, + isMutable: true, + editionNonce: { + __option: 'Some', + value: 255, + }, + tokenStandard: { + __option: 'None', + }, + collection: { + __option: 'None', + }, + uses: { + __option: 'None', + }, + collectionDetails: { + __option: 'None', + }, + programmableConfig: { + __option: 'None', + }, + }, + }, + { + publicKey: '8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY', + mint: { + publicKey: '8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY', + header: { + executable: false, + owner: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + lamports: { + basisPoints: 1461600, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + mintAuthority: { + __option: 'None', + }, + supply: 999771614895221, + decimals: 6, + isInitialized: true, + freezeAuthority: { + __option: 'None', + }, + }, + metadata: { + publicKey: 'EsDwUDmbeGAASS4HXd8v289fgVXDK182wsLnwLmkKLfD', + header: { + executable: false, + owner: 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', + lamports: { + basisPoints: 5616720, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + key: 4, + updateAuthority: 'TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM', + mint: '8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY', + name: 'KYOTO', + symbol: 'KYOTO', + uri: 'https://ipfs.io/ipfs/QmUzVaz4YH8sGpcPre2mgfwvXzT16gf6J6inYv2Z9Yn7f6', + sellerFeeBasisPoints: 0, + creators: { + __option: 'None', + }, + primarySaleHappened: false, + isMutable: false, + editionNonce: { + __option: 'Some', + value: 255, + }, + tokenStandard: { + __option: 'Some', + value: 2, + }, + collection: { + __option: 'None', + }, + uses: { + __option: 'None', + }, + collectionDetails: { + __option: 'None', + }, + programmableConfig: { + __option: 'None', + }, + }, + }, + ].filter((dg) => pks.findIndex((pk) => pk === dg.publicKey) !== -1) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const fetchDigitalAsset = async (_umi: any, pk: string) => { + if (pk === 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') { + return { + publicKey: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + mint: { + publicKey: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + header: { + executable: false, + owner: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + lamports: { + basisPoints: 87661392404, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + mintAuthority: { + __option: 'Some', + value: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + }, + supply: 1889938175280062, + decimals: 6, + isInitialized: true, + freezeAuthority: { + __option: 'Some', + value: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + }, + }, + metadata: { + publicKey: '8c3zk1t1qt3RU43ckuvPkCS7HLbjJqq3J3Me8ov4aHrp', + header: { + executable: false, + owner: 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', + lamports: { + basisPoints: 5616720, + identifier: 'SOL', + decimals: 9, + }, + rentEpoch: 18446744073709551616, + exists: true, + }, + key: 4, + updateAuthority: 'Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi', + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + name: 'USDT', + symbol: 'USDT', + uri: '', + sellerFeeBasisPoints: 0, + creators: { + __option: 'None', + }, + primarySaleHappened: false, + isMutable: true, + editionNonce: { + __option: 'Some', + value: 255, + }, + tokenStandard: { + __option: 'None', + }, + collection: { + __option: 'None', + }, + uses: { + __option: 'None', + }, + collectionDetails: { + __option: 'None', + }, + programmableConfig: { + __option: 'None', + }, + }, + } + } + + throw new Error('Can not find asset') +} + +const mplTtokenMetadataPackage = { + ...mplTtokenMetadataRealPackage, + fetchAllDigitalAsset, + fetchDigitalAsset, +} + +module.exports = mplTtokenMetadataPackage diff --git a/packages/xchain-solana/__mocks__/@metaplex-foundation/umi-bundle-defaults.ts b/packages/xchain-solana/__mocks__/@metaplex-foundation/umi-bundle-defaults.ts new file mode 100644 index 000000000..f51793311 --- /dev/null +++ b/packages/xchain-solana/__mocks__/@metaplex-foundation/umi-bundle-defaults.ts @@ -0,0 +1,17 @@ +const umiBundleDefaultsRealPackage = jest.requireActual('@metaplex-foundation/umi-bundle-defaults') + +class Umi { + public use(): Umi { + return new Umi() + } +} +const createUmi = (): Umi => { + return new Umi() +} + +const umiBundleDefaultsMockPackage = { + ...umiBundleDefaultsRealPackage, + createUmi, +} + +module.exports = umiBundleDefaultsMockPackage diff --git a/packages/xchain-solana/__mocks__/@solana/web3.js.ts b/packages/xchain-solana/__mocks__/@solana/web3.js.ts new file mode 100644 index 000000000..97326b1f5 --- /dev/null +++ b/packages/xchain-solana/__mocks__/@solana/web3.js.ts @@ -0,0 +1,387 @@ +import { AccountInfo, ParsedAccountData, ParsedTransactionWithMeta, PublicKey } from '@solana/web3.js' + +const realPackage = jest.requireActual('@solana/web3.js') + +class Connection { + async getBalance(pk: PublicKey): Promise { + if (pk.toBase58() === '94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') { + return 1_000_000_000 + } + return 0 + } + + async getParsedTokenAccountsByOwner(pk: PublicKey): Promise<{ + value: { + pubkey: PublicKey + account: AccountInfo + }[] + }> { + if (pk.toBase58() === '94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') { + return { + value: [ + { + account: { + data: { + parsed: { + info: { + isNative: false, + mint: '8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY', + owner: '94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v', + state: 'initialized', + tokenAmount: { + amount: '756181', + decimals: 6, + uiAmount: 0.756181, + uiAmountString: '0.756181', + }, + }, + type: 'account', + }, + program: 'spl-token', + space: 165, + }, + executable: false, + lamports: 2039280, + owner: new PublicKey('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v'), + rentEpoch: 18446744073709552000, + }, + pubkey: new PublicKey('8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY'), + }, + { + account: { + data: { + parsed: { + info: { + isNative: false, + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + owner: '94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v', + state: 'initialized', + tokenAmount: { + amount: '54587', + decimals: 6, + uiAmount: 0.054587, + uiAmountString: '0.054587', + }, + }, + type: 'account', + }, + program: 'spl-token', + space: 165, + }, + executable: false, + lamports: 2039280, + owner: new PublicKey('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v'), + rentEpoch: 18446744073709552000, + }, + pubkey: new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'), + }, + ], + } + } + return { + value: [], + } + } + + public async getParsedTransaction(signature: string): Promise { + if (signature === 'fakeNativeSignature') { + return { + blockTime: 1724933679, + meta: { + computeUnitsConsumed: 150, + err: null, + fee: 5000, + innerInstructions: [], + logMessages: [ + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success', + ], + postBalances: [139522247, 10000000, 1], + postTokenBalances: [], + preBalances: [144527247, 5000000, 1], + preTokenBalances: [], + }, + slot: 286544924, + transaction: { + message: { + accountKeys: [ + { + pubkey: new PublicKey('DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s'), + signer: true, + source: 'transaction', + writable: true, + }, + { + pubkey: new PublicKey('FH6wye9tmorZMXLisVx9ZpZXDKvcSgasJtJoCXizSn36'), + signer: false, + source: 'transaction', + writable: true, + }, + { + pubkey: new PublicKey('11111111111111111111111111111111'), + signer: false, + source: 'transaction', + writable: false, + }, + ], + instructions: [ + { + parsed: { + info: { + destination: 'FH6wye9tmorZMXLisVx9ZpZXDKvcSgasJtJoCXizSn36', + lamports: 5000000, + source: 'DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s', + }, + type: 'transfer', + }, + program: 'system', + programId: new PublicKey('11111111111111111111111111111111'), + }, + ], + recentBlockhash: 'BPsS7ZC39SijJZowDUXg7c7j7aNQxCMkEKgcNge9iWpE', + }, + signatures: ['fakeNativeSignature'], + }, + } + } + if (signature === 'fakeTokenSignature') { + return { + blockTime: 1724758709, + meta: { + computeUnitsConsumed: 25308, + err: null, + fee: 15000, + innerInstructions: [ + { + index: 0, + instructions: [ + { + parsed: { + info: { + extensionTypes: ['immutableOwner'], + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + }, + type: 'getAccountDataSize', + }, + program: 'spl-token', + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }, + { + parsed: { + info: { + lamports: 2039280, + newAccount: 'BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU', + owner: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + source: 'AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy', + space: 165, + }, + type: 'createAccount', + }, + program: 'system', + programId: new PublicKey('11111111111111111111111111111111'), + }, + { + parsed: { + info: { + account: 'BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU', + }, + type: 'initializeImmutableOwner', + }, + program: 'spl-token', + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }, + { + parsed: { + info: { + account: 'BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU', + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + owner: 'DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s', + }, + type: 'initializeAccount3', + }, + program: 'spl-token', + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }, + ], + }, + ], + logMessages: [ + 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]', + 'Program log: Create', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: GetAccountDataSize', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1622 of 394555 compute units', + 'Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program 11111111111111111111111111111111 invoke [2]', + 'Program 11111111111111111111111111111111 success', + 'Program log: Initialize the associated token account', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: InitializeImmutableOwner', + 'Program log: Please upgrade to SPL Token 2022 for immutable owner support', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 387915 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: InitializeAccount3', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4241 of 384031 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 20514 of 400000 compute units', + 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4644 of 379486 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + ], + postBalances: [598763210025, 2039280, 2039280, 731913600, 151576527, 87637392404, 1, 934087680, 1], + postTokenBalances: [ + { + accountIndex: 1, + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + owner: 'DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s', + uiTokenAmount: { + amount: '25009340', + decimals: 6, + uiAmount: 25.00934, + uiAmountString: '25.00934', + }, + }, + { + accountIndex: 2, + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + owner: 'AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy', + uiTokenAmount: { + amount: '169628681246', + decimals: 6, + uiAmount: 169628.681246, + uiAmountString: '169628.681246', + }, + }, + ], + preBalances: [598765264305, 0, 2039280, 731913600, 151576527, 87637392404, 1, 934087680, 1], + preTokenBalances: [ + { + accountIndex: 2, + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + owner: 'AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy', + uiTokenAmount: { + amount: '169653690586', + decimals: 6, + uiAmount: 169653.690586, + uiAmountString: '169653.690586', + }, + }, + ], + }, + slot: 286137085, + transaction: { + message: { + accountKeys: [ + { + pubkey: new PublicKey('AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy'), + signer: true, + source: 'transaction', + writable: true, + }, + { + pubkey: new PublicKey('BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU'), + signer: false, + source: 'transaction', + writable: true, + }, + { + pubkey: new PublicKey('Gjufi2NCUkgEoGgkJmjmASQgsxPhJQSD4CNrK2bst4J6'), + signer: false, + source: 'transaction', + writable: true, + }, + { + pubkey: new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'), + signer: false, + source: 'transaction', + writable: false, + }, + { + pubkey: new PublicKey('DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s'), + signer: false, + source: 'transaction', + writable: false, + }, + { + pubkey: new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'), + signer: false, + source: 'transaction', + writable: false, + }, + { + pubkey: new PublicKey('11111111111111111111111111111111'), + signer: false, + source: 'transaction', + writable: false, + }, + { + pubkey: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + signer: false, + source: 'transaction', + writable: false, + }, + { + pubkey: new PublicKey('ComputeBudget111111111111111111111111111111'), + signer: false, + source: 'transaction', + writable: false, + }, + ], + instructions: [ + { + parsed: { + info: { + account: 'BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU', + mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + source: 'AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy', + systemProgram: '11111111111111111111111111111111', + tokenProgram: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + wallet: 'DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s', + }, + type: 'create', + }, + program: 'spl-associated-token-account', + programId: new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'), + }, + { + parsed: { + info: { + amount: '25009340', + authority: 'AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy', + destination: 'BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU', + source: 'Gjufi2NCUkgEoGgkJmjmASQgsxPhJQSD4CNrK2bst4J6', + }, + type: 'transfer', + }, + program: 'spl-token', + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }, + { + accounts: [], + data: '3hd3odyyp3J7', + programId: new PublicKey('ComputeBudget111111111111111111111111111111'), + }, + ], + recentBlockhash: 'G8DWwpPqnN4boETeGiebkswqtGhw3xp6QtDSsehL4fVW', + }, + signatures: ['fakeTokenSignature'], + }, + } + } + + return null + } +} + +const mockPackage = { + ...realPackage, + Connection, +} + +module.exports = mockPackage diff --git a/packages/xchain-solana/__tests__/client.test.ts b/packages/xchain-solana/__tests__/client.test.ts index 98d38cfec..36a0ac216 100644 --- a/packages/xchain-solana/__tests__/client.test.ts +++ b/packages/xchain-solana/__tests__/client.test.ts @@ -1,4 +1,5 @@ -import { Network } from '@xchainjs/xchain-client' +import { Network, TxType } from '@xchainjs/xchain-client' +import { AnyAsset, Asset, TokenAsset, assetFromStringEx, assetToString } from '@xchainjs/xchain-util' import { Client, defaultSolanaParams } from '../src' @@ -12,12 +13,12 @@ describe('Solana client', () => { }) }) - it('Should validate address as valid', () => { - expect(client.validateAddress('G72oBA9cRYUzR8Q9oLvJcNRx5ovcDGFvHsbZKp1BT75W')).toBeTruthy() - }) - - it('Should validate address as invalid', () => { - expect(client.validateAddress('fakeAddress')).toBeFalsy() + describe('Asset', () => { + it('Should get native asset', () => { + const assetInfo = client.getAssetInfo() + expect(assetToString(assetInfo.asset)).toBe('SOL.SOL') + expect(assetInfo.decimal).toBe(9) + }) }) describe('Explorers', () => { @@ -97,4 +98,111 @@ describe('Solana client', () => { }) }) }) + + describe('Addresses', () => { + it('Should not get address without phrase', () => { + expect(async () => await client.getAddressAsync()).rejects.toThrowError('Phrase must be provided') + }) + + it('Should not get address sync method not be implemented', () => { + expect(() => client.getAddress()).toThrow('Sync method not supported') + }) + + it('Should get full derivation path with account 0', () => { + expect(client.getFullDerivationPath(0)).toBe(`m/44'/501'/0'`) + }) + + it('Should get full derivation path with account 1', () => { + expect(client.getFullDerivationPath(1)).toBe(`m/44'/501'/1'`) + }) + + it('Should validate address as valid', () => { + expect(client.validateAddress('G72oBA9cRYUzR8Q9oLvJcNRx5ovcDGFvHsbZKp1BT75W')).toBeTruthy() + }) + + it('Should validate address as invalid', () => { + expect(client.validateAddress('fakeAddress')).toBeFalsy() + }) + }) + + describe('Balances', () => { + it('Should get all balances', async () => { + const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v') + expect(balances.length).toBe(3) + expect(assetToString(balances[0].asset)).toBe('SOL.SOL') + expect(balances[0].amount.decimal).toBe(9) + expect(balances[0].amount.amount().toString()).toBe('1000000000') + expect(assetToString(balances[1].asset)).toBe('SOL.KYOTO-8zMTcsEFiB12NKrM5QXWL5pw1QMNJrAhH6Kh278YWFRY') + expect(balances[1].amount.decimal).toBe(6) + expect(balances[1].amount.amount().toString()).toBe('756181') + expect(assetToString(balances[2].asset)).toBe('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') + expect(balances[2].amount.decimal).toBe(6) + expect(balances[2].amount.amount().toString()).toBe('54587') + }) + + it('Should get balances filtering assets', async () => { + const balances = await client.getBalance('94bPUbh8iazbg2UgUDrmMkgWoZz9Q1H813JZifZRB35v', [ + assetFromStringEx('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') as TokenAsset, + ]) + expect(balances.length).toBe(2) + expect(assetToString(balances[0].asset)).toBe('SOL.SOL') + expect(balances[0].amount.decimal).toBe(9) + expect(balances[0].amount.amount().toString()).toBe('1000000000') + expect(assetToString(balances[1].asset)).toBe('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') + expect(balances[1].amount.decimal).toBe(6) + expect(balances[1].amount.amount().toString()).toBe('54587') + }) + }) + + describe('Transactions', () => { + it('Should get native transaction', async () => { + const tx = await client.getTransactionData('fakeNativeSignature') + expect(tx.hash).toBe('fakeNativeSignature') + expect(assetToString(tx.asset)).toBe('SOL.SOL') + expect(tx.date.getTime()).toBe(1724933679000) + expect(tx.type).toBe(TxType.Transfer) + expect(tx.from.length).toBe(1) + expect(tx.from[0].from).toBe('DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s') + expect(tx.from[0].asset).toBeDefined() + expect(assetToString(tx.from[0].asset as Asset)).toBe('SOL.SOL') + expect(tx.from[0].amount.amount().toString()).toBe('5005000') + expect(tx.from[0].amount.decimal).toBe(9) + expect(tx.to.length).toBe(1) + expect(tx.to[0].to).toBe('FH6wye9tmorZMXLisVx9ZpZXDKvcSgasJtJoCXizSn36') + expect(tx.to[0].asset).toBeDefined() + expect(assetToString(tx.to[0].asset as Asset)).toBe('SOL.SOL') + expect(tx.to[0].amount.amount().toString()).toBe('5000000') + expect(tx.to[0].amount.decimal).toBe(9) + }) + + it('Should get token transaction', async () => { + const tx = await client.getTransactionData('fakeTokenSignature') + expect(tx.hash).toBe('fakeTokenSignature') + expect(assetToString(tx.asset)).toBe('SOL.SOL') + expect(tx.date.getTime()).toBe(1724758709000) + expect(tx.type).toBe(TxType.Transfer) + expect(tx.from.length).toBe(2) + expect(tx.from[0].from).toBe('AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy') + expect(tx.from[0].asset).toBeDefined() + expect(assetToString(tx.from[0].asset as AnyAsset)).toBe('SOL.SOL') + expect(tx.from[0].amount.amount().toString()).toBe('2054280') + expect(tx.from[0].amount.decimal).toBe(9) + expect(tx.from[1].from).toBe('AaZkwhkiDStDcgrU37XAj9fpNLrD8Erz5PNkdm4k5hjy') + expect(tx.from[1].asset).toBeDefined() + expect(assetToString(tx.from[1].asset as AnyAsset)).toBe('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') + expect(tx.from[1].amount.amount().toString()).toBe('25009340') + expect(tx.from[1].amount.decimal).toBe(6) + expect(tx.to.length).toBe(2) + expect(tx.to[0].to).toBe('BfJjcYwnm8JmYg1AxquTHqtc35DJFt3swfQKEGbGj3CU') + expect(tx.to[0].asset).toBeDefined() + expect(assetToString(tx.to[0].asset as AnyAsset)).toBe('SOL.SOL') + expect(tx.to[0].amount.amount().toString()).toBe('2039280') + expect(tx.to[0].amount.decimal).toBe(9) + expect(tx.to[1].to).toBe('DTHVAEEC6cJyHsmYYmCQvX2eEtgoXSeyGoRhLZvcf62s') + expect(tx.to[1].asset).toBeDefined() + expect(assetToString(tx.to[1].asset as AnyAsset)).toBe('SOL.USDT-Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB') + expect(tx.to[1].amount.amount().toString()).toBe('25009340') + expect(tx.to[1].amount.decimal).toBe(6) + }) + }) }) diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 757bbdbd4..6d8030f8a 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -111,10 +111,22 @@ export class Client extends BaseXChainClient { return this.explorerProviders[this.getNetwork()].getExplorerTxUrl(txID) } + /** + * Get the full derivation path based on the wallet index. + * @param {number} walletIndex The HD wallet index + * @returns {string} The full derivation path + */ + public getFullDerivationPath(walletIndex: number): string { + if (!this.rootDerivationPaths) { + throw Error('Can not generate derivation path due to root derivation path is undefined') + } + return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` + } + /** * Get the current address asynchronously. * - * @param {number} index The index of the address. + * @param {number} index The index of the address. Default 0 * @returns {Address} The Solana address related to the index provided. * @throws {"Phrase must be provided"} Thrown if the phrase has not been set before. */ @@ -139,18 +151,6 @@ export class Client extends BaseXChainClient { return isAddress(address) } - /** - * Get the full derivation path based on the wallet index. - * @param {number} walletIndex The HD wallet index - * @returns {string} The full derivation path - */ - public getFullDerivationPath(walletIndex: number): string { - if (!this.rootDerivationPaths) { - throw Error('Can not generate derivation path due to root derivation path is undefined') - } - return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'` - } - /** * Retrieves the balance of a given address. * @param {Address} address - The address to retrieve the balance for. From f63bf99869832d85a024a3e2fd88147790f1d4da Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 2 Sep 2024 12:58:34 +0200 Subject: [PATCH 19/21] typo --- examples/solana/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/solana/package.json b/examples/solana/package.json index 7e05fce5e..df761fda1 100644 --- a/examples/solana/package.json +++ b/examples/solana/package.json @@ -23,4 +23,4 @@ "ts-node": "10.9.2", "typescript": "^5.0.4" } -} \ No newline at end of file +} From cdab945e98ce537a739a71d599fae4f6d8526254 Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Mon, 2 Sep 2024 13:38:19 +0200 Subject: [PATCH 20/21] Changeset version file --- .changeset/thin-apricots-explode.md | 5 +++++ packages/xchain-solana/package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-apricots-explode.md diff --git a/.changeset/thin-apricots-explode.md b/.changeset/thin-apricots-explode.md new file mode 100644 index 000000000..287b85718 --- /dev/null +++ b/.changeset/thin-apricots-explode.md @@ -0,0 +1,5 @@ +--- +'@xchainjs/xchain-solana': patch +--- + +First release version of the package diff --git a/packages/xchain-solana/package.json b/packages/xchain-solana/package.json index d160141a1..9c64df55d 100644 --- a/packages/xchain-solana/package.json +++ b/packages/xchain-solana/package.json @@ -1,6 +1,6 @@ { "name": "@xchainjs/xchain-solana", - "version": "0.0.1", + "version": "0.0.0", "description": "Solana client for XChainJS", "keywords": [ "Solana", @@ -48,4 +48,4 @@ "access": "public", "directory": "release/package" } -} +} \ No newline at end of file From b4b787fd9cd69f01e5796d43cba57194ba9269ca Mon Sep 17 00:00:00 2001 From: 0xp3gasus <0xp3gasus@proton.me> Date: Wed, 4 Sep 2024 18:12:06 +0200 Subject: [PATCH 21/21] Comments --- examples/solana/.codesandbox/tasks.json | 17 ++++++++--------- examples/solana/.devcontainer/devcontainer.json | 3 +-- examples/solana/package.json | 2 +- examples/solana/tsconfig.json | 9 +++++++-- packages/xchain-solana/src/utils.ts | 11 +++++------ 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/examples/solana/.codesandbox/tasks.json b/examples/solana/.codesandbox/tasks.json index 0428d7f95..fff1c37f8 100644 --- a/examples/solana/.codesandbox/tasks.json +++ b/examples/solana/.codesandbox/tasks.json @@ -1,10 +1,9 @@ { - // These tasks will run in order when initializing your CodeSandbox project. - "setupTasks": [ - { - "name": "Install Dependencies", - "command": "yarn install" - } - ] -} - \ No newline at end of file + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [ + { + "name": "Install Dependencies", + "command": "yarn install" + } + ] +} \ No newline at end of file diff --git a/examples/solana/.devcontainer/devcontainer.json b/examples/solana/.devcontainer/devcontainer.json index 10d754993..9cef189a7 100644 --- a/examples/solana/.devcontainer/devcontainer.json +++ b/examples/solana/.devcontainer/devcontainer.json @@ -1,5 +1,4 @@ { "name": "Devcontainer", "image": "ghcr.io/codesandbox/devcontainers/typescript-node:latest" -} - \ No newline at end of file +} \ No newline at end of file diff --git a/examples/solana/package.json b/examples/solana/package.json index df761fda1..7e05fce5e 100644 --- a/examples/solana/package.json +++ b/examples/solana/package.json @@ -23,4 +23,4 @@ "ts-node": "10.9.2", "typescript": "^5.0.4" } -} +} \ No newline at end of file diff --git a/examples/solana/tsconfig.json b/examples/solana/tsconfig.json index f6dc52d0e..7b9e0e966 100644 --- a/examples/solana/tsconfig.json +++ b/examples/solana/tsconfig.json @@ -1,10 +1,15 @@ { "compilerOptions": { - "module":"commonjs", + "module": "commonjs", "target": "es5", "noEmitOnError": true, "resolveJsonModule": true, "esModuleInterop": true, - "lib": ["es6", "dom", "es2016", "es2017"] + "lib": [ + "es6", + "dom", + "es2016", + "es2017" + ] } } \ No newline at end of file diff --git a/packages/xchain-solana/src/utils.ts b/packages/xchain-solana/src/utils.ts index b4e90fbfb..2993cd226 100644 --- a/packages/xchain-solana/src/utils.ts +++ b/packages/xchain-solana/src/utils.ts @@ -2,11 +2,10 @@ import { Cluster } from '@solana/web3.js' import { Network } from '@xchainjs/xchain-client' export const getSolanaNetwork = (network: Network): Cluster => { - switch (network) { - case Network.Mainnet: - case Network.Stagenet: - return 'mainnet-beta' - case Network.Testnet: - return 'testnet' + const networkMap: { [key in Network]: Cluster } = { + [Network.Mainnet]: 'mainnet-beta', + [Network.Stagenet]: 'mainnet-beta', + [Network.Testnet]: 'testnet', } + return networkMap[network] }