@dbricks/dbricks-ts
Version:
## Introduction
216 lines (215 loc) • 9.68 kB
JavaScript
;
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;