Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: solana standard tx parsing #7924

Merged
merged 4 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/unchained-client/openapitools.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@
"useSingleRequestParameter": true
}
},
"solana": {
"inputSpec": "https://raw.githubusercontent.com/shapeshift/unchained/develop/node/coinstacks/solana/api/src/swagger.json",
"generatorName": "typescript-fetch",
"output": "#{cwd}/src/generated/solana",
"enablePostProcessFile": true,
"reservedWordsMappings": {
"in": "in"
},
"additionalProperties": {
"supportsES6": "true",
"useSingleRequestParameter": true
}
},
"thorchain": {
"inputSpec": "https://raw.githubusercontent.com/shapeshift/unchained/develop/go/coinstacks/thorchain/api/swagger.json",
"generatorName": "typescript-fetch",
Expand Down
1 change: 1 addition & 0 deletions packages/unchained-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * as ws from './websocket'
export * as evm from './evm'
export * as utxo from './utxo'
export * as cosmossdk from './cosmossdk'
export * as solana from './solana'

export * as ethereum from './evm/ethereum'
export * as avalanche from './evm/avalanche'
Expand Down
5 changes: 5 additions & 0 deletions packages/unchained-client/src/solana/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { V1Api } from '../generated/solana'

export type Api = V1Api

export * from './parser'
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Tx } from '../../..'

const tx: Tx = {
txid: '3owXWn8Em7FE7Dyao3kPLkTPySiGGSSo9e7VGiWDifk6GfQRrm2JYHdHStBzVRr6b6o1PztbGpuDsXb8o2yPxoV3',
blockHeight: 293321352,
description:
'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV transferred 0.000000001 SOL to DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV.',
type: 'TRANSFER',
source: 'SYSTEM_PROGRAM',
fee: 25000,
feePayer: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
signature:
'3owXWn8Em7FE7Dyao3kPLkTPySiGGSSo9e7VGiWDifk6GfQRrm2JYHdHStBzVRr6b6o1PztbGpuDsXb8o2yPxoV3',
slot: 293321352,
timestamp: 1727896282,
tokenTransfers: [],
nativeTransfers: [
{
fromUserAccount: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
toUserAccount: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
amount: 1,
},
],
accountData: [
{
account: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
nativeBalanceChange: -25000,
tokenBalanceChanges: [],
},
{
account: 'ComputeBudget111111111111111111111111111111',
nativeBalanceChange: 0,
tokenBalanceChanges: [],
},
{
account: '11111111111111111111111111111111',
nativeBalanceChange: 0,
tokenBalanceChanges: [],
},
],
transactionError: null,
instructions: [
{
accounts: [],
data: '3gJqkocMWaMm',
programId: 'ComputeBudget111111111111111111111111111111',
innerInstructions: [],
},
{
accounts: [],
data: 'Fj2Eoy',
programId: 'ComputeBudget111111111111111111111111111111',
innerInstructions: [],
},
{
accounts: [
'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV',
],
data: '3Bxs412MvVNQj175',
programId: '11111111111111111111111111111111',
innerInstructions: [],
},
],
events: {
compressed: null,
nft: null,
swap: null,
},
}

export default { tx }
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Tx } from '../../..'

const tx: Tx = {
txid: 'qN3jbqvw2ypfmTVJuUiohgLQgV4mq8oZ6QzuKhNeM8MX1bdAxCK7EoXJbvBUD61mhGmrFr1KQi5FqgcadfYi7CS',
blockHeight: 294850279,
description:
'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW transferred 0.010000388 SOL to DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL.',
type: 'TRANSFER',
source: 'SYSTEM_PROGRAM',
fee: 5000,
feePayer: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW',
signature:
'qN3jbqvw2ypfmTVJuUiohgLQgV4mq8oZ6QzuKhNeM8MX1bdAxCK7EoXJbvBUD61mhGmrFr1KQi5FqgcadfYi7CS',
slot: 294850279,
timestamp: 1728580091,
tokenTransfers: [],
nativeTransfers: [
{
fromUserAccount: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW',
toUserAccount: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
amount: 10000388,
},
],
accountData: [
{
account: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW',
nativeBalanceChange: -10005388,
tokenBalanceChanges: [],
},
{
account: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
nativeBalanceChange: 10000388,
tokenBalanceChanges: [],
},
{
account: '11111111111111111111111111111111',
nativeBalanceChange: 0,
tokenBalanceChanges: [],
},
],
transactionError: null,
instructions: [
{
accounts: [
'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW',
'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
],
data: '3Bxs41dFLGCCYtUF',
programId: '11111111111111111111111111111111',
innerInstructions: [],
},
],
events: {
compressed: null,
nft: null,
swap: null,
},
}

export default { tx }
126 changes: 126 additions & 0 deletions packages/unchained-client/src/solana/parser/__tests__/solana.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { solanaChainId, solAssetId } from '@shapeshiftoss/caip'
import { beforeAll, describe, expect, it, vi } from 'vitest'

import { TransferType, TxStatus } from '../../../types'
import type { ParsedTx } from '../../parser'
import { TransactionParser } from '../index'
import solSelfSend from './mockData/solSelfSend'
import solStandard from './mockData/solStandard'

const txParser = new TransactionParser({ assetId: solAssetId, chainId: solanaChainId })

describe('parseTx', () => {
beforeAll(() => {
vi.clearAllMocks()
})

describe('standard', () => {
it('should be able to parse sol send', async () => {
const { tx } = solStandard
const address = 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW'

const expected: ParsedTx = {
address,
blockHash: tx.blockHash,
blockHeight: tx.blockHeight,
blockTime: tx.timestamp,
chainId: solanaChainId,
confirmations: 1,
fee: {
assetId: solAssetId,
value: '5000',
},
status: TxStatus.Confirmed,
transfers: [
{
assetId: solAssetId,
components: [{ value: '10000388' }],
from: address,
to: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
totalValue: '10000388',
type: TransferType.Send,
},
],
txid: tx.txid,
}

const actual = await txParser.parse(tx, address)

expect(actual).toEqual(expected)
})

it('should be able to parse sol receive', async () => {
const { tx } = solStandard
const address = 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL'

const expected: ParsedTx = {
address,
blockHash: tx.blockHash,
blockHeight: tx.blockHeight,
blockTime: tx.timestamp,
chainId: solanaChainId,
confirmations: 1,
status: TxStatus.Confirmed,
transfers: [
{
assetId: solAssetId,
components: [{ value: '10000388' }],
from: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW',
to: address,
totalValue: '10000388',
type: TransferType.Receive,
},
],
txid: tx.txid,
}

const actual = await txParser.parse(tx, address)

expect(actual).toEqual(expected)
})
})

describe('self send', () => {
it('should be able to parse sol', async () => {
const { tx } = solSelfSend
const address = 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV'

const expected: ParsedTx = {
txid: tx.txid,
blockHash: tx.blockHash,
blockHeight: tx.blockHeight,
blockTime: tx.timestamp,
address,
chainId: solanaChainId,
confirmations: 1,
status: TxStatus.Confirmed,
fee: {
value: '25000',
assetId: solAssetId,
},
transfers: [
{
type: TransferType.Send,
from: address,
to: address,
assetId: solAssetId,
totalValue: '1',
components: [{ value: '1' }],
},
{
type: TransferType.Receive,
from: address,
to: address,
assetId: solAssetId,
totalValue: '1',
components: [{ value: '1' }],
},
],
}

const actual = await txParser.parse(tx, address)

expect(actual).toEqual(expected)
})
})
})
98 changes: 98 additions & 0 deletions packages/unchained-client/src/solana/parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { AssetId, ChainId } from '@shapeshiftoss/caip'

import { TransferType, TxStatus } from '../../types'
import { aggregateTransfer } from '../../utils'
import type { ParsedTx, SubParser, Tx } from './types'

export * from './types'

export interface TransactionParserArgs {
chainId: ChainId
assetId: AssetId
}

export class TransactionParser<T extends Tx> {
chainId: ChainId
assetId: AssetId

private parsers: SubParser<T>[] = []

constructor(args: TransactionParserArgs) {
this.chainId = args.chainId
this.assetId = args.assetId
}

/**
* Register custom transaction sub parser to parse custom op return data
*
* _parsers should be registered from most generic first to most specific last_
*/
registerParser(parser: SubParser<T>): void {
this.parsers.unshift(parser)
}

protected registerParsers(parsers: SubParser<T>[]): void {
parsers.forEach(parser => this.registerParser(parser))
}

async parse(tx: T, address: string): Promise<ParsedTx> {
const parserResult = await (async () => {
for (const parser of this.parsers) {
const result = await parser.parse(tx, address)
if (result) return result
}
})()

const parsedTx: ParsedTx = {
address,
blockHeight: tx.blockHeight,
blockTime: tx.timestamp,
chainId: this.chainId,
confirmations: 1,
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
status: this.getStatus(tx),
trade: parserResult?.trade,
transfers: parserResult?.transfers ?? [],
txid: tx.txid,
}

// network fee
if (tx.feePayer === address && tx.fee) {
parsedTx.fee = { assetId: this.assetId, value: BigInt(tx.fee).toString() }
}

tx.nativeTransfers?.forEach(nativeTransfer => {
const { amount, fromUserAccount, toUserAccount } = nativeTransfer

// send amount
if (nativeTransfer.fromUserAccount === address) {
parsedTx.transfers = aggregateTransfer({
assetId: this.assetId,
from: fromUserAccount ?? '',
to: toUserAccount ?? '',
transfers: parsedTx.transfers,
type: TransferType.Send,
value: BigInt(amount).toString(),
})
}

// receive amount
if (nativeTransfer.toUserAccount === address) {
parsedTx.transfers = aggregateTransfer({
assetId: this.assetId,
from: fromUserAccount ?? '',
to: toUserAccount ?? '',
transfers: parsedTx.transfers,
type: TransferType.Receive,
value: BigInt(amount).toString(),
})
}
})

return parsedTx
}

private getStatus(tx: T): TxStatus {
if (tx.transactionError) return TxStatus.Failed
return TxStatus.Confirmed
}
}
Loading
Loading