UNPKG

@dbricks/dbricks-ts

Version:

## Introduction

216 lines (215 loc) 9.68 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Builder = exports.builderEmitter = void 0; /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ const web3_js_1 = require("@solana/web3.js"); const axios_1 = __importDefault(require("axios")); const winston_1 = __importDefault(require("winston")); const events_1 = __importDefault(require("events")); const common_serializers_1 = require("../common.serializers"); const common_builder_types_1 = require("./common.builder.types"); const common_utils_1 = require("../common.utils"); exports.builderEmitter = new events_1.default.EventEmitter.EventEmitter(); function logAndEmit(event, msg) { winston_1.default.debug(msg); exports.builderEmitter.emit(event, msg); } class Builder { connection; ownerPubkey; baseUrl; // not all methods require communicating with the api, hence it's ok for this to be blank apiKey; rawBricks = []; constructor({ ownerPubkey, connectionUrl, committment, baseUrl = common_builder_types_1.DEFAULT_BASE_URL, apiKey, } = {}) { this.connection = new web3_js_1.Connection(connectionUrl, committment); this.baseUrl = baseUrl; this.ownerPubkey = ownerPubkey; this.apiKey = apiKey; } // --------------------------------------- core methods addBrick(brick) { this.rawBricks.push(brick); } parseBricks(rawBricks) { const parsedBricks = rawBricks.map((b) => { // @ts-ignore const endpoint = common_builder_types_1.endpoints[b.protocol][b.action]; const [method, route] = endpoint.split(' '); return { protocol: b.protocol, action: b.action, method: method, route, args: b.args, }; }); logAndEmit('parseBricks', `Parsed bricks: ${parsedBricks}`); return parsedBricks; } async fetchBricks(parsedBricks) { const fetchedBricks = []; const requests = []; logAndEmit('fetchBricks', 'Connecting to dbricks server, stand by...'); parsedBricks.forEach((b) => { const request = (0, axios_1.default)({ baseURL: this.baseUrl, method: b.method, url: b.route, data: { ...b.args, ownerPubkey: this.ownerPubkey.toBase58(), }, }); requests.push(request); fetchedBricks.push({ protocol: b.protocol, action: b.action, instructionsAndSigners: [], }); }); const responses = await axios_1.default.all(requests); for (let i = 0; i < responses.length; i += 1) { fetchedBricks[i].instructionsAndSigners = (0, common_serializers_1.deserializeInstructionsAndSigners)(responses[i].data); } logAndEmit('fetchBricks', `Fetched bricks from server: ${fetchedBricks}`); return fetchedBricks; } flattenBricks(fetchedBricks) { const flattenedBricks = []; fetchedBricks.forEach((b) => { b.instructionsAndSigners.forEach((i) => { if (i.instructions.length > 0) { flattenedBricks.push({ protocol: b.protocol, action: b.action, instructionsAndSigners: i, }); } }); }); logAndEmit('flattenBricks', `Flattened bricks: ${flattenedBricks}`); return flattenedBricks; } async optimallySizeBricks(flattenedBricks) { logAndEmit('optimallySizeBricks', `Attempting transaction with ${flattenedBricks.length} bricks.`); const attemptedBrick = { protocols: [], actions: [], transaction: new web3_js_1.Transaction(), signers: [], }; flattenedBricks.forEach((b) => { attemptedBrick.protocols.push(b.protocol); attemptedBrick.actions.push(b.action); attemptedBrick.transaction.add(...b.instructionsAndSigners.instructions); attemptedBrick.signers.push(...b.instructionsAndSigners.signers); }); attemptedBrick.transaction.recentBlockhash = (await this.connection.getRecentBlockhash()) .blockhash; attemptedBrick.transaction.feePayer = this.ownerPubkey; try { const buf = attemptedBrick.transaction.serialize({ verifySignatures: false, }); logAndEmit('optimallySizeBricks', `Transaction of size ${buf.length} fits ${flattenedBricks.length} bricks just ok.`); return [attemptedBrick]; } catch (e) { const middle = Math.ceil(flattenedBricks.length / 2); logAndEmit('optimallySizeBricks', `Transaction with ${flattenedBricks.length} bricks is too large, breaking into 2 at ${middle}.`); const left = flattenedBricks.splice(0, middle); const right = flattenedBricks.splice(-middle); return [ ...(await this.optimallySizeBricks(left)), ...(await this.optimallySizeBricks(right)), ]; } } // if the exact same transaction has been executed before, we need to update the blockhash // otherwise it will fail with "This transaction has already been processed" async updateBlockhashOnSimilarTransactions(sizedBricks) { const sizedBricksCopy = [...sizedBricks]; const stringifiedBricks = []; for (const brick of sizedBricksCopy) { while (brick.transaction.recentBlockhash && stringifiedBricks.indexOf(this.stringifySizedBrick(brick)) !== -1) { await (0, common_utils_1.asyncTimeout)(2000); brick.transaction.recentBlockhash = (await this.connection.getRecentBlockhash()).blockhash; } stringifiedBricks.push(this.stringifySizedBrick(brick)); } logAndEmit('updateBlockhashOnSimilarTransactions', 'Blockhash refreshed to prevent duplicates.'); return sizedBricksCopy; } async executeBricks(sizedBricks, keypair, connectedAdapter, signCallback, callbackArgs) { if (!keypair && !connectedAdapter && !signCallback) { throw new Error('You need to pass one of: 1)sign callback, 2)wallet adapter, 3)owner keypair'); } for (const b of sizedBricks) { try { let sig; if (keypair) { sig = await this.sendWithKeypair(b.transaction, keypair, b.signers); } else if (connectedAdapter) { sig = await this.sendWithWallet(b.transaction, connectedAdapter, b.signers); } else if (signCallback) { sig = await this.sendWithCallback(b.transaction, signCallback, callbackArgs, b.signers); } logAndEmit('executeBricks', `Transaction successful, ${sig}.`); for (let i = 0; i < b.protocols.length; i += 1) { logAndEmit('executeBricks', `${b.protocols[i]}/${b.actions[i]} brick executed.`); } } catch (e) { const msg = `Transaction failed, ${e}`; logAndEmit('executeBricksError', msg); // we don't want to continue the loop if we have a transaction failure. throw new Error(msg); } } } async sendWithKeypair(transaction, keypair, additionalSigners) { transaction.sign(keypair, ...additionalSigners); return this.connection.sendRawTransaction(transaction.serialize()); } async sendWithWallet(transaction, connectedAdapter, additionalSigners) { logAndEmit('executeBricksSign', 'Please sign the transaction'); const sig = await connectedAdapter.sendTransaction(transaction, this.connection, { signers: additionalSigners, }); await this.connection.confirmTransaction(sig, 'processed'); return sig; } async sendWithCallback(transaction, signCallback, callbackArgs = [], additionalSigners) { const signedTransaction = await signCallback(transaction, ...callbackArgs); if (additionalSigners.length > 0) { signedTransaction.partialSign(...additionalSigners); } return this.connection.sendRawTransaction(signedTransaction.serialize()); } async build({ keypair, connectedAdapter, signCallback, callbackArgs, } = {}) { const parsedBricks = this.parseBricks(this.rawBricks); const fetchedBricks = await this.fetchBricks(parsedBricks); const flattenedBricks = this.flattenBricks(fetchedBricks); const sizedBricks = await this.optimallySizeBricks(flattenedBricks); const finalBricks = await this.updateBlockhashOnSimilarTransactions(sizedBricks); await this.executeBricks(finalBricks, keypair, connectedAdapter, signCallback, callbackArgs); } // --------------------------------------- helpers stringifySizedBrick(brick) { let result = ''; brick.transaction.instructions.forEach((instruction) => { result += instruction.data.toString(); }); result += brick.transaction.recentBlockhash; return result; } } exports.Builder = Builder;