From 17d30fdaa103d95482349e972d8df6048988799d Mon Sep 17 00:00:00 2001 From: Marcus Pasell Date: Tue, 19 Jul 2022 22:54:15 +0000 Subject: [PATCH 1/2] SDK: Convert TransactionHandler to TypeScript and export on core --- libs/src/core.ts | 1 + ...actionHandler.js => transactionHandler.ts} | 205 +++++++++++------- 2 files changed, 128 insertions(+), 78 deletions(-) rename libs/src/services/solanaWeb3Manager/{transactionHandler.js => transactionHandler.ts} (63%) diff --git a/libs/src/core.ts b/libs/src/core.ts index 9ce1571f21f..d154fb6e5c1 100644 --- a/libs/src/core.ts +++ b/libs/src/core.ts @@ -1,3 +1,4 @@ export * from './constants' export * as IdentityAPI from './services/identity/requests' export * as DiscoveryAPI from './services/discoveryProvider/requests' +export { TransactionHandler } from './services/solanaWeb3Manager/transactionHandler' diff --git a/libs/src/services/solanaWeb3Manager/transactionHandler.js b/libs/src/services/solanaWeb3Manager/transactionHandler.ts similarity index 63% rename from libs/src/services/solanaWeb3Manager/transactionHandler.js rename to libs/src/services/solanaWeb3Manager/transactionHandler.ts index c822d01bfaf..f3a598eee62 100644 --- a/libs/src/services/solanaWeb3Manager/transactionHandler.js +++ b/libs/src/services/solanaWeb3Manager/transactionHandler.ts @@ -1,33 +1,30 @@ -const SolanaUtils = require('./utils') -const { +import SolanaUtils from './utils' +import { Transaction, PublicKey, -} = require('@solana/web3.js') + Connection, + Keypair, + TransactionInstruction +} from '@solana/web3.js' /** * Handles sending Solana transactions, either directly via `sendAndConfirmTransaction`, * or via IdentityService's relay. */ -class TransactionHandler { +export class TransactionHandler { + private readonly connection: Connection + private readonly useRelay: boolean + private readonly identityService: any | null + private readonly feePayerKeypairs: Keypair[] | null + private readonly skipPreflight: boolean + private readonly retryTimeoutMs: number + private readonly pollingFrequencyMs: number + private readonly sendingFrequencyMs: number + /** * Creates an instance of TransactionHandler. - * - * @param {{ - * connection: Connection, - * useRelay: boolean, - * identityService: Object, - * feePayerKeypairs: KeyPair[] - * skipPreflight: boolean - * }} { - * connection, - * useRelay, - * identityService = null, - * feePayerKeypairs = null, - * skipPreflight = true - * } - * @memberof TransactionHandler */ - constructor ({ + constructor({ connection, useRelay, identityService = null, @@ -36,6 +33,15 @@ class TransactionHandler { retryTimeoutMs = 60000, pollingFrequencyMs = 300, sendingFrequencyMs = 300 + }: { + connection: Connection + useRelay: boolean + identityService?: any | null + feePayerKeypairs?: Keypair[] | null + skipPreflight?: boolean + retryTimeoutMs?: number + pollingFrequencyMs?: number + sendingFrequencyMs?: number }) { this.connection = connection this.useRelay = useRelay @@ -49,44 +55,68 @@ class TransactionHandler { /** * Primary method to send a Solana transaction. - * - * @typedef {Object} HandleTransactionReturn - * @property {Object} res the result - * @property {string} [error=null] the optional error - * Will be a string if `errorMapping` is passed to the handler. - * @property {string|number} [error_code=null] the optional error code. - * @property {string} [recentBlockhash=null] optional recent blockhash to prefer over fetching - * @property {boolean} [skipPreflight=null] optional per transaction override to skipPreflight - * @property {any} [logger=console] optional logger - * @property {any} [feePayerOverride=null] optional fee payer override - * - * @param {Array} instructions an array of `TransactionInstructions` - * @param {*} [errorMapping=null] an optional error mapping. Should expose a `fromErrorCode` method. - * @param {Array<{publicKey: string, signature: Buffer}>} [signature=null] optional signatures - * @returns {Promise} - * @memberof TransactionHandler */ - async handleTransaction ({ instructions, errorMapping = null, recentBlockhash = null, logger = console, skipPreflight = null, feePayerOverride = null, sendBlockhash = true, signatures = null, retry = true }) { - let result = null + async handleTransaction({ + instructions, + errorMapping = null, + recentBlockhash = null, + logger = console, + skipPreflight = false, + feePayerOverride = null, + sendBlockhash = true, + signatures = null, + retry = true + }: { + instructions: TransactionInstruction[] + errorMapping?: { fromErrorCode: (errorCode: number) => string } | null + recentBlockhash?: string | null + logger?: any + skipPreflight?: boolean + feePayerOverride?: string | null + sendBlockhash?: boolean + signatures?: Array<{ publicKey: string; signature: Buffer }> | null + retry?: boolean + }) { + let result: { + res: any + errorCode: string | number | null + error: string | null + } | null = null if (this.useRelay) { - result = await this._relayTransaction(instructions, recentBlockhash, skipPreflight, feePayerOverride, sendBlockhash, signatures, retry) + result = await this._relayTransaction( + instructions, + recentBlockhash, + skipPreflight, + feePayerOverride, + sendBlockhash, + signatures, + retry + ) } else { - result = await this._locallyConfirmTransaction(instructions, recentBlockhash, logger, skipPreflight, feePayerOverride, signatures, retry) + result = await this._locallyConfirmTransaction( + instructions, + recentBlockhash, + logger, + skipPreflight, + feePayerOverride, + signatures, + retry + ) } if (result.error && result.errorCode !== null && errorMapping) { - result.errorCode = errorMapping.fromErrorCode(result.errorCode) + result.errorCode = errorMapping.fromErrorCode(result.errorCode as number) } return result } - async _relayTransaction ( - instructions, - recentBlockhash, - skipPreflight, - feePayerOverride = null, - sendBlockhash, - signatures, - retry + async _relayTransaction( + instructions: TransactionInstruction[], + recentBlockhash: string | null, + skipPreflight: boolean, + feePayerOverride: string | null = null, + sendBlockhash: boolean, + signatures: Array<{ publicKey: string; signature: Buffer }> | null, + retry: boolean ) { const relayable = instructions.map(SolanaUtils.prepareInstructionForRelay) @@ -96,25 +126,35 @@ class TransactionHandler { skipPreflight: skipPreflight === null ? this.skipPreflight : skipPreflight, feePayerOverride: feePayerOverride ? feePayerOverride.toString() : null, - retry + retry, + recentBlockhash: undefined as string | undefined } if (sendBlockhash || Array.isArray(signatures)) { - transactionData.recentBlockhash = (recentBlockhash || (await this.connection.getLatestBlockhash('confirmed')).blockhash) + transactionData.recentBlockhash = + recentBlockhash ?? + (await this.connection.getLatestBlockhash('confirmed')).blockhash } try { const response = await this.identityService.solanaRelay(transactionData) return { res: response, error: null, errorCode: null } - } catch (e) { - const error = - (e.response && e.response.data && e.response.data.error) || e.message + } catch (e: any) { + const error = e.response?.data?.error || e.message const errorCode = this._parseSolanaErrorCode(error) return { res: null, error, errorCode } } } - async _locallyConfirmTransaction (instructions, recentBlockhash, logger, skipPreflight, feePayerOverride = null, signatures = null, retry = true) { + async _locallyConfirmTransaction( + instructions: TransactionInstruction[], + recentBlockhash: string | null, + logger: any, + skipPreflight: boolean, + feePayerOverride: string | null = null, + signatures: Array<{ publicKey: string; signature: Buffer }> | null = null, + retry = true + ) { const feePayerKeypairOverride = (() => { if (feePayerOverride && this.feePayerKeypairs) { const stringFeePayer = feePayerOverride.toString() @@ -125,9 +165,12 @@ class TransactionHandler { return null })() - const feePayerAccount = feePayerKeypairOverride || (this.feePayerKeypairs && this.feePayerKeypairs[0]) + const feePayerAccount = + feePayerKeypairOverride ?? this.feePayerKeypairs?.[0] if (!feePayerAccount) { - logger.error('transactionHandler: Local feepayer keys missing for direct confirmation!') + logger.error( + 'transactionHandler: Local feepayer keys missing for direct confirmation!' + ) return { res: null, error: 'Missing keys', @@ -138,7 +181,7 @@ class TransactionHandler { // Get blockhash recentBlockhash = - recentBlockhash || + recentBlockhash ?? (await this.connection.getLatestBlockhash('confirmed')).blockhash // Construct the txn @@ -159,10 +202,9 @@ class TransactionHandler { // Send the txn const sendRawTransaction = async () => { - return this.connection.sendRawTransaction(rawTransaction, { + return await this.connection.sendRawTransaction(rawTransaction, { skipPreflight: skipPreflight === null ? this.skipPreflight : skipPreflight, - commitment: 'processed', preflightCommitment: 'processed', maxRetries: retry ? 0 : undefined }) @@ -174,7 +216,7 @@ class TransactionHandler { } catch (e) { // Rarely, this intiial send will fail logger.warn(`transactionHandler: Initial send failed: ${e}`) - const { message: error } = e + const { message: error } = e as any const errorCode = this._parseSolanaErrorCode(error) return { res: null, @@ -196,7 +238,9 @@ class TransactionHandler { try { sendRawTransaction() } catch (e) { - logger.warn(`transactionHandler: error in send loop: ${e} for txId ${txid}`) + logger.warn( + `transactionHandler: error in send loop: ${e} for txId ${txid}` + ) } sendCount++ await delay(this.sendingFrequencyMs) @@ -209,16 +253,22 @@ class TransactionHandler { try { await this._awaitTransactionSignatureConfirmation(txid, logger) done = true - logger.info(`transactionHandler: finished for txid ${txid} with ${sendCount} retries`) + logger.info( + `transactionHandler: finished for txid ${txid} with ${sendCount} retries` + ) return { res: txid, error: null, errorCode: null } } catch (e) { - logger.warn(`transactionHandler: error in awaitTransactionSignature: ${JSON.stringify(e)}, ${txid}`) + logger.warn( + `transactionHandler: error in awaitTransactionSignature: ${JSON.stringify( + e + )}, ${txid}` + ) done = true - const { message: error } = e + const { message: error } = e as any const errorCode = this._parseSolanaErrorCode(error) return { res: null, @@ -228,7 +278,7 @@ class TransactionHandler { } } - async _awaitTransactionSignatureConfirmation (txid, logger) { + async _awaitTransactionSignatureConfirmation(txid: string, logger: any) { let done = false const result = await new Promise((resolve, reject) => { @@ -253,7 +303,9 @@ class TransactionHandler { done = true if (result.err) { const err = JSON.stringify(result.err) - logger.warn(`transactionHandler: Error in onSignature ${txid}, ${err}`) + logger.warn( + `transactionHandler: Error in onSignature ${txid}, ${err}` + ) reject(new Error(err)) } else { resolve(txid) @@ -291,16 +343,14 @@ class TransactionHandler { // Early return if response without confirmation if ( !( - result.confirmations || + (result.confirmations !== null && + result.confirmations !== 0) || result.confirmationStatus === 'confirmed' || result.confirmationStatus === 'finalized' ) ) { return } - - // Otherwise, we made it - done = true resolve(txid) } catch (e) { if (!done) { @@ -324,22 +374,21 @@ class TransactionHandler { * "... custom program error: 0x1", where the return in this case would be the number 1. * Returns null for unparsable strings. */ - _parseSolanaErrorCode (errorMessage) { + _parseSolanaErrorCode(errorMessage: string) { if (!errorMessage) return null // Match on custom solana program errors const matcher = /(?:custom program error: 0x)(.*)$/ const res = errorMessage.match(matcher) - if (res && res.length === 2) return parseInt(res[1], 16) || null + if (res && res.length === 2) return parseInt(res[1] as string, 16) || null // Match on custom anchor errors const matcher2 = /(?:"Custom":)(\d+)/ const res2 = errorMessage.match(matcher2) - if (res2 && res2.length === 2) return parseInt(res2[1], 10) || null + if (res2 && res2.length === 2) + return parseInt(res2[1] as string, 10) || null return null } } -async function delay (ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) +async function delay(ms: number) { + return await new Promise((resolve) => setTimeout(resolve, ms)) } - -module.exports = { TransactionHandler } From 7e20cebe552c6de475067c1122cc2c85d1e60c0d Mon Sep 17 00:00:00 2001 From: Marcus Pasell Date: Thu, 21 Jul 2022 03:52:16 +0000 Subject: [PATCH 2/2] Match Identity relay response, update error safety, revert removed done=true, reduce any usage --- libs/src/services/identity/IdentityService.ts | 33 ++++------ .../solanaWeb3Manager/transactionHandler.ts | 66 ++++++++++++------- 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/libs/src/services/identity/IdentityService.ts b/libs/src/services/identity/IdentityService.ts index 35de931da61..f3119ee081d 100644 --- a/libs/src/services/identity/IdentityService.ts +++ b/libs/src/services/identity/IdentityService.ts @@ -7,6 +7,7 @@ import { getTrackListens, TimeFrame } from './requests' import type { Web3Manager } from '../web3Manager' import type { TransactionReceipt } from 'web3-core' import type Wallet from 'ethereumjs-wallet' +import type { TransactionInstruction } from '@solana/web3.js' type Data = Record @@ -24,23 +25,13 @@ export type RelayTransaction = { } } -type TransactionData = { - recentBlockhash: string - secpInstruction?: { - publicKey: string - message: string - signature: any - recoveryId: number - } - instruction: { - keys: Array<{ - pubkey: string - isSigner?: boolean - isWritable?: boolean - }> - programId: string - data: Record - } +export type RelayTransactionData = { + instructions: TransactionInstruction[] + skipPreflight: boolean + feePayerOverride: string | null + signatures?: Array<{ publicKey: string; signature: Buffer }> | null + retry?: boolean + recentBlockhash?: string } type AttestationResult = { @@ -456,10 +447,10 @@ export class IdentityService { } // Relays tx data through the solana relay endpoint - async solanaRelay(transactionData: TransactionData) { + async solanaRelay(transactionData: RelayTransactionData) { const headers = await this._signData() - return await this._makeRequest({ + return await this._makeRequest<{ transactionSignature: string }>({ url: '/solana/relay', method: 'post', data: transactionData, @@ -467,8 +458,8 @@ export class IdentityService { }) } - async solanaRelayRaw(transactionData: TransactionData) { - return await this._makeRequest({ + async solanaRelayRaw(transactionData: RelayTransactionData) { + return await this._makeRequest<{ transactionSignature: string }>({ url: '/solana/relay/raw', method: 'post', data: transactionData diff --git a/libs/src/services/solanaWeb3Manager/transactionHandler.ts b/libs/src/services/solanaWeb3Manager/transactionHandler.ts index f3a598eee62..222852c6b83 100644 --- a/libs/src/services/solanaWeb3Manager/transactionHandler.ts +++ b/libs/src/services/solanaWeb3Manager/transactionHandler.ts @@ -6,6 +6,9 @@ import { Keypair, TransactionInstruction } from '@solana/web3.js' +import type { IdentityService, RelayTransactionData } from '../identity' + +type Logger = Pick /** * Handles sending Solana transactions, either directly via `sendAndConfirmTransaction`, @@ -14,7 +17,7 @@ import { export class TransactionHandler { private readonly connection: Connection private readonly useRelay: boolean - private readonly identityService: any | null + private readonly identityService: IdentityService | null private readonly feePayerKeypairs: Keypair[] | null private readonly skipPreflight: boolean private readonly retryTimeoutMs: number @@ -36,7 +39,7 @@ export class TransactionHandler { }: { connection: Connection useRelay: boolean - identityService?: any | null + identityService?: IdentityService | null feePayerKeypairs?: Keypair[] | null skipPreflight?: boolean retryTimeoutMs?: number @@ -70,15 +73,15 @@ export class TransactionHandler { instructions: TransactionInstruction[] errorMapping?: { fromErrorCode: (errorCode: number) => string } | null recentBlockhash?: string | null - logger?: any - skipPreflight?: boolean + logger?: Logger + skipPreflight?: boolean | null feePayerOverride?: string | null sendBlockhash?: boolean signatures?: Array<{ publicKey: string; signature: Buffer }> | null retry?: boolean }) { let result: { - res: any + res: string | { transactionSignature: string } | null errorCode: string | number | null error: string | null } | null = null @@ -112,7 +115,7 @@ export class TransactionHandler { async _relayTransaction( instructions: TransactionInstruction[], recentBlockhash: string | null, - skipPreflight: boolean, + skipPreflight: boolean | null, feePayerOverride: string | null = null, sendBlockhash: boolean, signatures: Array<{ publicKey: string; signature: Buffer }> | null, @@ -120,14 +123,13 @@ export class TransactionHandler { ) { const relayable = instructions.map(SolanaUtils.prepareInstructionForRelay) - const transactionData = { + const transactionData: RelayTransactionData = { signatures, instructions: relayable, skipPreflight: skipPreflight === null ? this.skipPreflight : skipPreflight, feePayerOverride: feePayerOverride ? feePayerOverride.toString() : null, - retry, - recentBlockhash: undefined as string | undefined + retry } if (sendBlockhash || Array.isArray(signatures)) { @@ -137,11 +139,18 @@ export class TransactionHandler { } try { - const response = await this.identityService.solanaRelay(transactionData) - return { res: response, error: null, errorCode: null } - } catch (e: any) { - const error = e.response?.data?.error || e.message - const errorCode = this._parseSolanaErrorCode(error) + const response = await this.identityService?.solanaRelay(transactionData) + return { + res: response ?? null, + error: null, + errorCode: null + } + } catch (e) { + let error = null + if (typeof e === 'object' && e !== null) { + error = (e as any).response?.data?.error || (e as Error).message + } + const errorCode = error ? this._parseSolanaErrorCode(error) : null return { res: null, error, errorCode } } } @@ -149,8 +158,8 @@ export class TransactionHandler { async _locallyConfirmTransaction( instructions: TransactionInstruction[], recentBlockhash: string | null, - logger: any, - skipPreflight: boolean, + logger: Logger, + skipPreflight: boolean | null, feePayerOverride: string | null = null, signatures: Array<{ publicKey: string; signature: Buffer }> | null = null, retry = true @@ -216,8 +225,12 @@ export class TransactionHandler { } catch (e) { // Rarely, this intiial send will fail logger.warn(`transactionHandler: Initial send failed: ${e}`) - const { message: error } = e as any - const errorCode = this._parseSolanaErrorCode(error) + let errorCode = null + let error = null + if (e instanceof Error) { + error = e.message + errorCode = this._parseSolanaErrorCode(error) + } return { res: null, error, @@ -268,8 +281,12 @@ export class TransactionHandler { )}, ${txid}` ) done = true - const { message: error } = e as any - const errorCode = this._parseSolanaErrorCode(error) + let errorCode = null + let error = null + if (e instanceof Error) { + error = e.message + errorCode = this._parseSolanaErrorCode(error) + } return { res: null, error, @@ -278,7 +295,7 @@ export class TransactionHandler { } } - async _awaitTransactionSignatureConfirmation(txid: string, logger: any) { + async _awaitTransactionSignatureConfirmation(txid: string, logger: Logger) { let done = false const result = await new Promise((resolve, reject) => { @@ -351,6 +368,8 @@ export class TransactionHandler { ) { return } + // Otherwise, we made it + done = true resolve(txid) } catch (e) { if (!done) { @@ -379,12 +398,13 @@ export class TransactionHandler { // Match on custom solana program errors const matcher = /(?:custom program error: 0x)(.*)$/ const res = errorMessage.match(matcher) - if (res && res.length === 2) return parseInt(res[1] as string, 16) || null + if (res && res.length === 2) + return res[1] ? parseInt(res[1], 16) || null : null // Match on custom anchor errors const matcher2 = /(?:"Custom":)(\d+)/ const res2 = errorMessage.match(matcher2) if (res2 && res2.length === 2) - return parseInt(res2[1] as string, 10) || null + return res2[1] ? parseInt(res2[1], 10) || null : null return null } }