Client for sending interactions to the sequencer
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
"test:integration:basic": "jest ./src/__tests__/integration/basic",
|
||||
"test:integration:basic:load": "jest --silent=false --detectOpenHandles ./src/__tests__/integration/basic/contract-loading.test.ts ",
|
||||
"test:integration:basic:arweave": "jest ./src/__tests__/integration/basic/arweave-transactions-loading",
|
||||
"test:integration:decentralized-sequencer": "jest ./src/__tests__/integration/decentralized-sequencer --detectOpenHandles",
|
||||
"test:integration:internal-writes": "jest ./src/__tests__/integration/internal-writes",
|
||||
"test:integration:wasm": "jest ./src/__tests__/integration/wasm",
|
||||
"test:regression": "node ./node_modules/.bin/jest ./src/__tests__/regression",
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import ArLocal from 'arlocal';
|
||||
import Arweave from 'arweave';
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createServer, Server } from 'http';
|
||||
import { DeployPlugin, ArweaveSigner } from 'warp-contracts-plugin-deploy';
|
||||
import { Contract, WriteInteractionResponse } from '../../../contract/Contract';
|
||||
import { Warp } from '../../../core/Warp';
|
||||
import { WarpFactory, defaultCacheOptions, defaultWarpGwOptions } from '../../../core/WarpFactory';
|
||||
import { SourceType } from '../../../core/modules/impl/WarpGatewayInteractionsLoader';
|
||||
import { AddressInfo } from 'net';
|
||||
import { WARP_TAGS } from '../../../core/KnownTags';
|
||||
|
||||
interface ExampleContractState {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
// FIXME: change to the address of the sequencer on dev
|
||||
const DECENTRALIZED_SEQUENCER_URL = 'http://sequencer-0.warp.cc:1317';
|
||||
|
||||
describe('Testing sending of interactions to a decentralized sequencer', () => {
|
||||
let contractSrc: string;
|
||||
let initialState: string;
|
||||
let wallet: JWKInterface;
|
||||
let arlocal: ArLocal;
|
||||
let warp: Warp;
|
||||
let contract: Contract<ExampleContractState>;
|
||||
let sequencerServer: Server;
|
||||
let centralizedSeqeuencerUrl: string;
|
||||
let centralizedSequencerType: boolean;
|
||||
|
||||
beforeAll(async () => {
|
||||
const port = 1813;
|
||||
arlocal = new ArLocal(port, false);
|
||||
await arlocal.start();
|
||||
|
||||
const arweave = Arweave.init({
|
||||
host: 'localhost',
|
||||
port: port,
|
||||
protocol: 'http'
|
||||
});
|
||||
|
||||
// a mock server simulating a centralized sequencer
|
||||
centralizedSequencerType = false;
|
||||
sequencerServer = createServer((req, res) => {
|
||||
if (req.url === '/gateway/sequencer/address') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
url: centralizedSequencerType ? centralizedSeqeuencerUrl : DECENTRALIZED_SEQUENCER_URL,
|
||||
type: centralizedSequencerType ? 'centralized' : 'decentralized'
|
||||
}));
|
||||
return;
|
||||
} else if (req.url === '/gateway/v2/sequencer/register') {
|
||||
centralizedSequencerType = false;
|
||||
res.writeHead(301, { Location: DECENTRALIZED_SEQUENCER_URL });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
throw new Error("Unexpected sequencer path: " + req.url);
|
||||
})
|
||||
await new Promise<void>(resolve => {
|
||||
sequencerServer.listen(() => {
|
||||
const address = sequencerServer.address() as AddressInfo
|
||||
centralizedSeqeuencerUrl = `http://localhost:${address.port}`
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
const cacheOptions = {
|
||||
...defaultCacheOptions,
|
||||
inMemory: true
|
||||
}
|
||||
const gatewayOptions = { ...defaultWarpGwOptions, source: SourceType.WARP_SEQUENCER, confirmationStatus: { notCorrupted: true } }
|
||||
|
||||
warp = WarpFactory
|
||||
.custom(arweave, cacheOptions, 'custom')
|
||||
.useWarpGateway(gatewayOptions, cacheOptions)
|
||||
.build()
|
||||
.use(new DeployPlugin());
|
||||
|
||||
({ jwk: wallet } = await warp.generateWallet());
|
||||
|
||||
contractSrc = fs.readFileSync(path.join(__dirname, '../data/example-contract.js'), 'utf8');
|
||||
initialState = fs.readFileSync(path.join(__dirname, '../data/example-contract-state.json'), 'utf8');
|
||||
|
||||
const { contractTxId } = await warp.deploy({
|
||||
wallet: new ArweaveSigner(wallet),
|
||||
initState: initialState,
|
||||
src: contractSrc
|
||||
});
|
||||
|
||||
contract = warp.contract<ExampleContractState>(contractTxId).setEvaluationOptions({
|
||||
sequencerUrl: centralizedSeqeuencerUrl
|
||||
});
|
||||
contract.connect(wallet);
|
||||
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await arlocal.stop();
|
||||
await new Promise(resolve => {
|
||||
sequencerServer.close(resolve)
|
||||
})
|
||||
});
|
||||
|
||||
const getNonceFromResult = (result: WriteInteractionResponse | null): number => {
|
||||
if (result) {
|
||||
for (let tag of result.interactionTx.tags) {
|
||||
if (tag.name === WARP_TAGS.SEQUENCER_NONCE) {
|
||||
return Number(tag.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
it('should add new interactions waiting for confirmation from the sequencer', async () => {
|
||||
contract.setEvaluationOptions({ waitForConfirmation: true })
|
||||
|
||||
await contract.writeInteraction({ function: 'add' });
|
||||
const result = await contract.writeInteraction({ function: 'add' });
|
||||
expect(getNonceFromResult(result)).toEqual(1)
|
||||
expect(result?.bundlrResponse).toBeUndefined();
|
||||
expect(result?.sequencerTxHash).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add new interactions without waiting for confirmation from the sequencer', async () => {
|
||||
contract.setEvaluationOptions({ waitForConfirmation: false })
|
||||
|
||||
await contract.writeInteraction({ function: 'add' });
|
||||
const result = await contract.writeInteraction({ function: 'add' });
|
||||
expect(getNonceFromResult(result)).toEqual(3)
|
||||
expect(result?.bundlrResponse).toBeUndefined();
|
||||
expect(result?.sequencerTxHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should follow the redirection returned by the centralized sequencer.', async () => {
|
||||
centralizedSequencerType = true;
|
||||
contract.setEvaluationOptions({
|
||||
sequencerUrl: centralizedSeqeuencerUrl,
|
||||
waitForConfirmation: true
|
||||
});
|
||||
|
||||
const result = await contract.writeInteraction({ function: 'add' });
|
||||
expect(getNonceFromResult(result)).toEqual(4)
|
||||
expect(result?.bundlrResponse).toBeUndefined();
|
||||
expect(result?.sequencerTxHash).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import Arweave from 'arweave';
|
||||
import { createData, DataItem, Signer } from 'warp-arbundles';
|
||||
import { ArweaveSigner } from 'warp-contracts-plugin-deploy';
|
||||
import { DecentralizedSequencerClient } from '../../../contract/sequencer/DecentralizedSequencerClient';
|
||||
import { SMART_WEAVE_TAGS, WARP_TAGS } from '../../../core/KnownTags';
|
||||
import { Tag } from '../../../utils/types/arweave-types';
|
||||
import { WarpFactory } from '../../../core/WarpFactory';
|
||||
import { WarpFetchWrapper } from '../../../core/WarpFetchWrapper';
|
||||
import { Signature } from '../../../contract/Signature';
|
||||
|
||||
// FIXME: change to the address of the sequencer on dev
|
||||
const SEQUENCER_URL = 'http://sequencer-0.warp.cc:1317';
|
||||
|
||||
describe('Testing a decentralized sequencer client', () => {
|
||||
let client: DecentralizedSequencerClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
const warpFetchWrapper = new WarpFetchWrapper(WarpFactory.forLocal())
|
||||
client = new DecentralizedSequencerClient(SEQUENCER_URL, warpFetchWrapper);
|
||||
});
|
||||
|
||||
const createSignature = async (): Promise<Signature> => {
|
||||
const wallet = await Arweave.crypto.generateJWK();
|
||||
const signer = new ArweaveSigner(wallet);
|
||||
return new Signature(WarpFactory.forLocal(), signer)
|
||||
}
|
||||
|
||||
const createDataItem = async (signature: Signature, nonce: number, addNonceTag = true, addContractTag = true, signDataItem = true): Promise<DataItem> => {
|
||||
const signer = signature.bundlerSigner;
|
||||
const tags: Tag[] = [];
|
||||
if (addNonceTag) {
|
||||
tags.push(new Tag(WARP_TAGS.SEQUENCER_NONCE, String(nonce)));
|
||||
}
|
||||
if (addContractTag) {
|
||||
tags.push(new Tag(SMART_WEAVE_TAGS.CONTRACT_TX_ID, "unit test contract"));
|
||||
}
|
||||
const dataItem = createData('some data', signer, { tags });
|
||||
if (signDataItem) {
|
||||
await dataItem.sign(signer);
|
||||
}
|
||||
return dataItem;
|
||||
}
|
||||
|
||||
it('should return consecutive nonces for a given signature', async () => {
|
||||
const signature = await createSignature()
|
||||
let nonce = await client.getNonce(signature);
|
||||
expect(nonce).toEqual(0);
|
||||
|
||||
nonce = await client.getNonce(signature);
|
||||
expect(nonce).toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject a data item with an invalid nonce', async () => {
|
||||
const signature = await createSignature()
|
||||
const dataItem = await createDataItem(signature, 13);
|
||||
|
||||
expect(client.sendDataItem(dataItem, false))
|
||||
.rejects
|
||||
.toThrowError('account sequence mismatch, expected 0, got 13: incorrect account sequence');
|
||||
});
|
||||
|
||||
it('should reject a data item without nonce', async () => {
|
||||
const signature = await createSignature()
|
||||
const dataItem = await createDataItem(signature, 0, false);
|
||||
|
||||
expect(client.sendDataItem(dataItem, true))
|
||||
.rejects
|
||||
.toThrowError('no sequencer nonce tag');
|
||||
});
|
||||
|
||||
it('should reject a data item without contract', async () => {
|
||||
const signature = await createSignature()
|
||||
const dataItem = await createDataItem(signature, 0, true, false);
|
||||
|
||||
expect(client.sendDataItem(dataItem, true))
|
||||
.rejects
|
||||
.toThrowError('no contract tag');
|
||||
});
|
||||
|
||||
it('should reject an unsigned data item', async () => {
|
||||
const signature = await createSignature()
|
||||
const dataItem = await createDataItem(signature, 0, true, true, false);
|
||||
|
||||
expect(client.sendDataItem(dataItem, true))
|
||||
.rejects
|
||||
.toThrowError('data item verification error');
|
||||
});
|
||||
|
||||
it('should return a confirmed result', async () => {
|
||||
const signature = await createSignature();
|
||||
const nonce = await client.getNonce(signature);
|
||||
const dataItem = await createDataItem(signature, nonce);
|
||||
const result = await client.sendDataItem(dataItem, true);
|
||||
|
||||
expect(result.sequencerMoved).toEqual(false);
|
||||
expect(result.bundlrResponse).toBeUndefined();
|
||||
expect(result.sequencerTxHash).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return an unconfirmed result', async () => {
|
||||
const signature = await createSignature();
|
||||
const nonce = await client.getNonce(signature);
|
||||
const dataItem = await createDataItem(signature, nonce);
|
||||
const result = await client.sendDataItem(dataItem, false);
|
||||
|
||||
expect(result.sequencerMoved).toEqual(false);
|
||||
expect(result.bundlrResponse).toBeUndefined();
|
||||
expect(result.sequencerTxHash).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { Transaction } from '../utils/types/arweave-types';
|
||||
|
||||
export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number };
|
||||
|
||||
interface BundlrResponse {
|
||||
export interface BundlrResponse {
|
||||
id: string;
|
||||
public: string;
|
||||
signature: string;
|
||||
@@ -23,6 +23,7 @@ export interface WriteInteractionResponse {
|
||||
bundlrResponse?: BundlrResponse;
|
||||
originalTxId: string;
|
||||
interactionTx: Transaction | DataItem;
|
||||
sequencerTxHash?: string;
|
||||
}
|
||||
|
||||
export interface DREContractStatusResponse<State> {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Benchmark } from '../logging/Benchmark';
|
||||
import { LoggerFactory } from '../logging/LoggerFactory';
|
||||
import { Evolve } from '../plugins/Evolve';
|
||||
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
|
||||
import { getJsonResponse, isBrowser, sleep, stripTrailingSlash } from '../utils/utils';
|
||||
import { getJsonResponse, isBrowser, sleep } from '../utils/utils';
|
||||
import {
|
||||
BenchmarkStats,
|
||||
Contract,
|
||||
@@ -41,6 +41,7 @@ import { ContractInteractionState } from './states/ContractInteractionState';
|
||||
import { Crypto } from 'warp-isomorphic';
|
||||
import { VrfPluginFunctions } from '../core/WarpPlugin';
|
||||
import { createData, tagsExceedLimit, DataItem, Signer } from 'warp-arbundles';
|
||||
import { SequencerClient, createSequencerClient } from './sequencer/SequencerClient';
|
||||
|
||||
/**
|
||||
* An implementation of {@link Contract} that is backwards compatible with current style
|
||||
@@ -72,6 +73,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
private _children: HandlerBasedContract<unknown>[] = [];
|
||||
private _interactionState;
|
||||
private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();
|
||||
private _sequencerClient: SequencerClient;
|
||||
|
||||
constructor(
|
||||
private readonly _contractTxId: string,
|
||||
@@ -325,7 +327,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
tags: Tags;
|
||||
strict: boolean;
|
||||
vrf: boolean;
|
||||
}
|
||||
},
|
||||
sequencerRedirected = false
|
||||
): Promise<WriteInteractionResponse | null> {
|
||||
this.logger.info('Bundle interaction input', input);
|
||||
|
||||
@@ -337,27 +340,37 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
options.vrf
|
||||
);
|
||||
|
||||
const response = this._warpFetchWrapper.fetch(
|
||||
`${stripTrailingSlash(this._evaluationOptions.sequencerUrl)}/gateway/v2/sequencer/register`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: interactionDataItem.getRaw()
|
||||
}
|
||||
const sequencerClient = await this.getSequencerClient();
|
||||
const sendResponse = await sequencerClient.sendDataItem(
|
||||
interactionDataItem,
|
||||
this._evaluationOptions.waitForConfirmation
|
||||
);
|
||||
|
||||
const dataItemId = await interactionDataItem.id;
|
||||
if (sendResponse.sequencerMoved) {
|
||||
this.logger.info(
|
||||
`The sequencer at the given address (${this._evaluationOptions.sequencerUrl}) is redirecting to a new sequencer`
|
||||
);
|
||||
if (sequencerRedirected) {
|
||||
throw new Error('Too many sequencer redirects');
|
||||
}
|
||||
this._sequencerClient = null;
|
||||
return this.bundleInteraction(input, options, true);
|
||||
}
|
||||
|
||||
return {
|
||||
bundlrResponse: await getJsonResponse(response),
|
||||
originalTxId: dataItemId,
|
||||
interactionTx: interactionDataItem
|
||||
bundlrResponse: sendResponse.bundlrResponse,
|
||||
originalTxId: await interactionDataItem.id,
|
||||
interactionTx: interactionDataItem,
|
||||
sequencerTxHash: sendResponse.sequencerTxHash
|
||||
};
|
||||
}
|
||||
|
||||
private async getSequencerClient(): Promise<SequencerClient> {
|
||||
if (!this._sequencerClient) {
|
||||
this._sequencerClient = await createSequencerClient(this._evaluationOptions.sequencerUrl, this._warpFetchWrapper);
|
||||
}
|
||||
return this._sequencerClient;
|
||||
}
|
||||
|
||||
private async createInteractionDataItem<Input>(
|
||||
input: Input,
|
||||
tags: Tags,
|
||||
@@ -370,6 +383,12 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf);
|
||||
}
|
||||
|
||||
const sequencerClient = await this.getSequencerClient();
|
||||
const nonce = await sequencerClient.getNonce(this._signature);
|
||||
if (nonce !== undefined) {
|
||||
tags.push(new Tag(WARP_TAGS.SEQUENCER_NONCE, String(nonce)));
|
||||
}
|
||||
|
||||
if (vrf) {
|
||||
tags.push(new Tag(WARP_TAGS.REQUEST_VRF, 'true'));
|
||||
}
|
||||
@@ -483,6 +502,9 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
if (!this.isRoot()) {
|
||||
throw new Error('Evaluation options can be set only for the root contract');
|
||||
}
|
||||
if (options.sequencerUrl) {
|
||||
this._sequencerClient = null;
|
||||
}
|
||||
this._evaluationOptions = {
|
||||
...this._evaluationOptions,
|
||||
...options
|
||||
|
||||
@@ -29,6 +29,7 @@ export class Signature {
|
||||
private readonly signatureProviderType: 'CustomSignature' | 'ArWallet' | 'BundlerSigner';
|
||||
private readonly wallet;
|
||||
private cachedAddress?: string;
|
||||
sequencerNonce: number;
|
||||
|
||||
constructor(warp: Warp, walletOrSignature: SignatureProvider) {
|
||||
this.warp = warp;
|
||||
|
||||
61
src/contract/sequencer/CentralizedSequencerClient.ts
Normal file
61
src/contract/sequencer/CentralizedSequencerClient.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { BundlrResponse } from 'contract/Contract';
|
||||
import { WarpFetchWrapper } from 'core/WarpFetchWrapper';
|
||||
import { NetworkCommunicationError, getJsonResponse } from '../../utils/utils';
|
||||
import { DataItem } from 'warp-arbundles';
|
||||
import { SendDataItemResponse, SequencerClient } from './SequencerClient';
|
||||
|
||||
/**
|
||||
* Client for a centralized sequencer.
|
||||
*/
|
||||
export class CentralizedSequencerClient implements SequencerClient {
|
||||
private registerUrl: string;
|
||||
private warpFetchWrapper: WarpFetchWrapper;
|
||||
|
||||
constructor(sequencerUrl: string, warpFetchWrapper: WarpFetchWrapper) {
|
||||
this.registerUrl = `${sequencerUrl}/gateway/v2/sequencer/register`;
|
||||
this.warpFetchWrapper = warpFetchWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sequencer does not have a nonce mechanism; therefore, the method returns undefined.
|
||||
* @returns undefined
|
||||
*/
|
||||
getNonce(): Promise<number> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* It sends an interaction to the sequencer and checks if the response has a status of 301 (Moved Permanently).
|
||||
*/
|
||||
async sendDataItem(dataItem: DataItem): Promise<SendDataItemResponse> {
|
||||
const result = this.warpFetchWrapper.fetch(this.registerUrl, {
|
||||
redirect: 'manual',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: dataItem.getRaw()
|
||||
});
|
||||
return getJsonResponse<SendDataItemResponse>(
|
||||
result,
|
||||
(result) => {
|
||||
return {
|
||||
bundlrResponse: result as BundlrResponse,
|
||||
sequencerMoved: false
|
||||
};
|
||||
},
|
||||
async (response) => {
|
||||
if (response.status == 301) {
|
||||
return {
|
||||
bundlrResponse: undefined,
|
||||
sequencerMoved: true
|
||||
};
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
throw new NetworkCommunicationError(`Wrong response code: ${response.status}. ${text}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
211
src/contract/sequencer/DecentralizedSequencerClient.ts
Normal file
211
src/contract/sequencer/DecentralizedSequencerClient.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import base64url from 'base64url';
|
||||
import { DataItem } from 'warp-arbundles';
|
||||
import { getJsonResponse, NetworkCommunicationError, sleep } from '../../utils/utils';
|
||||
import { LoggerFactory } from '../../logging/LoggerFactory';
|
||||
import { WarpFetchWrapper } from '../../core/WarpFetchWrapper';
|
||||
import { SendDataItemResponse, SequencerClient } from './SequencerClient';
|
||||
import { Signature } from 'contract/Signature';
|
||||
|
||||
type NonceResponse = {
|
||||
address: string;
|
||||
nonce: number;
|
||||
};
|
||||
|
||||
type CheckTxResponse = {
|
||||
confirmed: boolean;
|
||||
txHash?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client for a decentralized sequencer.
|
||||
*/
|
||||
export class DecentralizedSequencerClient implements SequencerClient {
|
||||
private readonly logger = LoggerFactory.INST.create('DecentralizedSequencerClient');
|
||||
|
||||
private nonceUrl: string;
|
||||
private sendDataItemUrl: string;
|
||||
private getTxUrl: string;
|
||||
private warpFetchWrapper: WarpFetchWrapper;
|
||||
|
||||
constructor(sequencerUrl: string, warpFetchWrapper: WarpFetchWrapper) {
|
||||
this.nonceUrl = `${sequencerUrl}/api/v1/nonce`;
|
||||
this.sendDataItemUrl = `${sequencerUrl}/api/v1/dataitem`;
|
||||
this.getTxUrl = `${sequencerUrl}/api/v1/tx-data-item-id`;
|
||||
this.warpFetchWrapper = warpFetchWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sequence (nonce) for an account owned by a given signer. The result is stored in the signature class's counter.
|
||||
* For subsequent interactions, the nonce will be retrieved from the signature's counter without communication with the sequencer.
|
||||
*
|
||||
* @param signature the signature for which the nonce is calculated
|
||||
* @returns nonce
|
||||
*/
|
||||
async getNonce(signature: Signature): Promise<number> {
|
||||
const nonce = signature.sequencerNonce;
|
||||
if (nonce !== undefined) {
|
||||
signature.sequencerNonce = nonce + 1;
|
||||
return nonce;
|
||||
}
|
||||
|
||||
return this.fetchNonce(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* It retrieves the nonce from the sequencer for the next interaction.
|
||||
*/
|
||||
private async fetchNonce(signature: Signature): Promise<number> {
|
||||
const bundlerSigner = signature.bundlerSigner;
|
||||
if (!bundlerSigner) {
|
||||
throw new Error(
|
||||
'Signer not set correctly. To use the decentralized sequencer, one should use the BundlerSigner type.'
|
||||
);
|
||||
}
|
||||
|
||||
const signatureType = bundlerSigner.signatureType;
|
||||
const owner = base64url.encode(bundlerSigner.publicKey);
|
||||
|
||||
const response = this.warpFetchWrapper.fetch(this.nonceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ signature_type: signatureType, owner })
|
||||
});
|
||||
|
||||
const nonceResponse = await getJsonResponse<NonceResponse>(response);
|
||||
this.logger.info('Nonce for owner', { owner, nonceResponse });
|
||||
signature.sequencerNonce = nonceResponse.nonce + 1;
|
||||
return nonceResponse.nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a data item to the sequencer network and optionally monitoring its inclusion in the blockchain.
|
||||
* If the broadcasting is rejected by the node (e.g., during the CheckTx method), an error is thrown.
|
||||
* If the option to wait for confirmation is selected,
|
||||
* the hash of the sequencer transaction containing the interaction is returned.
|
||||
*
|
||||
* @param dataItem data item to be sent
|
||||
* @param waitForConfirmation whether to wait for confirmation that data item has been included in the blockchain
|
||||
* @returns hash of the sequencer transaction if wait for confirmation is selected
|
||||
*/
|
||||
async sendDataItem(dataItem: DataItem, waitForConfirmation: boolean): Promise<SendDataItemResponse> {
|
||||
const response = await this.sendDataItemWithRetry(dataItem);
|
||||
|
||||
if (waitForConfirmation) {
|
||||
response.sequencerTxHash = await this.confirmTx(await dataItem.id);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a data item to the sequencer.
|
||||
* It retries in case of 'Service Unavailable' status and throws an error if the interaction is rejected by the sequencer.
|
||||
*
|
||||
* @param dataItem data item to be sent
|
||||
* @param numberOfTries the number of retries
|
||||
*/
|
||||
private async sendDataItemWithRetry(dataItem: DataItem, numberOfTries = 20): Promise<SendDataItemResponse> {
|
||||
if (numberOfTries <= 0) {
|
||||
throw new Error(
|
||||
`Failed to send the interaction (id = ${await dataItem.id}) to the sequencer despite multiple retries`
|
||||
);
|
||||
}
|
||||
|
||||
if (await this.tryToSendDataItem(dataItem)) {
|
||||
return { sequencerMoved: false };
|
||||
} else {
|
||||
await sleep(1000);
|
||||
return this.sendDataItemWithRetry(dataItem, numberOfTries - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private async tryToSendDataItem(dataItem: DataItem): Promise<boolean> {
|
||||
const response = this.warpFetchWrapper.fetch(this.sendDataItemUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
body: dataItem.getRaw()
|
||||
});
|
||||
|
||||
return getJsonResponse<boolean>(
|
||||
response,
|
||||
() => true,
|
||||
async (response) => {
|
||||
if (response.status == 503) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.status == 409) {
|
||||
const error = await response.json();
|
||||
throw new Error(
|
||||
`Interaction (id = ${await dataItem.id}) rejected by the sequencer due to an invalid nonce, error message: ${
|
||||
error.message.RawLog
|
||||
}}`
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status == 400) {
|
||||
const error = await response.json();
|
||||
throw new Error(
|
||||
`Interaction (id = ${await dataItem.id}) rejected by the sequencer: error type: ${
|
||||
error.type
|
||||
}, error message: ${JSON.stringify(error.message)}`
|
||||
);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
throw new NetworkCommunicationError(`Wrong response code: ${response.status}. ${text}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* It queries the sequencer every second to check if the data item is in the chain
|
||||
*
|
||||
* @param dataItem data item to be sent
|
||||
* @param numberOfTries the number of retries
|
||||
*/
|
||||
private async confirmTx(dataItemId: string, numberOfTries = 20): Promise<string> {
|
||||
if (numberOfTries <= 0) {
|
||||
throw new Error(`Failed to confirm of the interaction with id = ${dataItemId} in the sequencer network`);
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
const result = await this.checkTx(dataItemId);
|
||||
if (!result.confirmed) {
|
||||
return this.confirmTx(dataItemId, numberOfTries - 1);
|
||||
}
|
||||
return result.txHash;
|
||||
}
|
||||
|
||||
private async checkTx(dataItemId: string): Promise<CheckTxResponse> {
|
||||
const response = this.warpFetchWrapper.fetch(this.getTxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ data_item_id: dataItemId })
|
||||
});
|
||||
|
||||
return getJsonResponse<CheckTxResponse>(
|
||||
response,
|
||||
(result) => {
|
||||
this.logger.info(`The transaction with hash ${result.tx_hash} confirmed.`);
|
||||
return { confirmed: true, txHash: result.tx_hash };
|
||||
},
|
||||
async (response) => {
|
||||
if (response.status == 404) {
|
||||
this.logger.debug(`The transaction with data item id (${dataItemId}) not confirmed yet.`);
|
||||
return { confirmed: false };
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
throw new NetworkCommunicationError(`${response.status}: ${text}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
84
src/contract/sequencer/SequencerClient.ts
Normal file
84
src/contract/sequencer/SequencerClient.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { BundlrResponse } from 'contract/Contract';
|
||||
import { Signature } from 'contract/Signature';
|
||||
import { getJsonResponse, stripTrailingSlash } from '../../utils/utils';
|
||||
import { DataItem } from 'warp-arbundles';
|
||||
import { CentralizedSequencerClient } from './CentralizedSequencerClient';
|
||||
import { DecentralizedSequencerClient } from './DecentralizedSequencerClient';
|
||||
import { WarpFetchWrapper } from 'core/WarpFetchWrapper';
|
||||
|
||||
/**
|
||||
* The return type of sending an interaction to the sequencer
|
||||
*/
|
||||
export type SendDataItemResponse = {
|
||||
/**
|
||||
* Whether the sequencer returned a "Moved Permanently" status with the address of the new sequencer
|
||||
*/
|
||||
sequencerMoved: boolean;
|
||||
/**
|
||||
* The response from the bundler if the sequencer sends an interaction there
|
||||
*/
|
||||
bundlrResponse?: BundlrResponse;
|
||||
/**
|
||||
* The transaction hash in the decentralized sequencer blockchain containing the data item
|
||||
*/
|
||||
sequencerTxHash?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client for connecting to the sequencer, including sending interactions to the sequencer.
|
||||
*/
|
||||
export interface SequencerClient {
|
||||
/**
|
||||
* It returns the nonce for the next interaction signed by a given signer.
|
||||
* If the sequencer does not support nonces, it returns undefined.
|
||||
*/
|
||||
getNonce(signature: Signature): Promise<number>;
|
||||
|
||||
/**
|
||||
* It sends an interaction in the form of a data item to the sequencer.
|
||||
* Potentially waits for confirmation that the interaction has been included in the sequencer chain.
|
||||
*
|
||||
* @param dataItem interaction in the form of a data item
|
||||
* @param waitForConfirmation whether to wait for confirmation that the interaction has been included in the chain
|
||||
*/
|
||||
sendDataItem(dataItem: DataItem, waitForConfirmation: boolean): Promise<SendDataItemResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The response type from an endpoint returning the address of the current sequencer.
|
||||
*/
|
||||
type SequencerAddress = {
|
||||
/**
|
||||
* The URL address of the sequencer
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* The type of sequencer
|
||||
*/
|
||||
type: 'centralized' | 'decentralized';
|
||||
};
|
||||
|
||||
/**
|
||||
* It queries an endpoint with an address and sequencer type, and returns a client for that sequencer.
|
||||
*
|
||||
* @param sequencerUrl URL address with an endpoint that returns the sequencer's address
|
||||
* @param warpFetchWrapper wrapper for fetch operation
|
||||
* @returns client for the sequencer
|
||||
*/
|
||||
export const createSequencerClient = async (
|
||||
sequencerUrl: string,
|
||||
warpFetchWrapper: WarpFetchWrapper
|
||||
): Promise<SequencerClient> => {
|
||||
const response = warpFetchWrapper.fetch(`${stripTrailingSlash(sequencerUrl)}/gateway/sequencer/address`);
|
||||
const address = await getJsonResponse<SequencerAddress>(response);
|
||||
|
||||
if (address.type == 'centralized') {
|
||||
return new CentralizedSequencerClient(address.url, warpFetchWrapper);
|
||||
}
|
||||
|
||||
if (address.type == 'decentralized') {
|
||||
return new DecentralizedSequencerClient(address.url, warpFetchWrapper);
|
||||
}
|
||||
|
||||
throw new Error('Unknown sequencer type: ' + address.type);
|
||||
};
|
||||
@@ -28,6 +28,7 @@ export const WARP_TAGS = {
|
||||
SEQUENCER_BLOCK_HEIGHT: 'Sequencer-Block-Height',
|
||||
SEQUENCER_BLOCK_ID: 'Sequencer-Block-Id',
|
||||
SEQUENCER_BLOCK_TIMESTAMP: 'Sequencer-Block-Timestamp',
|
||||
SEQUENCER_NONCE: 'Sequencer-Nonce',
|
||||
INIT_STATE: 'Init-State',
|
||||
INIT_STATE_TX: 'Init-State-TX',
|
||||
INTERACT_WRITE: 'Interact-Write',
|
||||
|
||||
@@ -159,8 +159,9 @@ export interface EvaluationOptions {
|
||||
// whether exceptions from given transaction interaction should be ignored
|
||||
ignoreExceptions: boolean;
|
||||
|
||||
// allow to wait for confirmation of the interaction transaction - this way
|
||||
// you will know, when the new interaction is effectively available on the network
|
||||
// Allows waiting for confirmation of the interaction.
|
||||
// In the case of the 'disableBundling' option, the confirmation comes from the Arweave network,
|
||||
// otherwise from the decentralized Warp Sequencer.
|
||||
waitForConfirmation: boolean;
|
||||
|
||||
// whether the state cache should be updated after evaluating each interaction transaction.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import copy from 'fast-copy';
|
||||
import { Buffer } from 'warp-isomorphic';
|
||||
import { KnownErrors } from '../core/modules/impl/handler/JsHandlerApi';
|
||||
|
||||
export const sleep = (ms: number): Promise<void> => {
|
||||
@@ -95,7 +94,7 @@ export class NetworkCommunicationError<T> extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJsonResponse<T>(response: Promise<Response>): Promise<T> {
|
||||
export async function getJsonResponse<T>(response: Promise<Response>, successCallback?: (result: any) => T, errorCallback?: (response: Response) => Promise<T>): Promise<T> {
|
||||
let r: Response;
|
||||
try {
|
||||
r = await response;
|
||||
@@ -104,12 +103,18 @@ export async function getJsonResponse<T>(response: Promise<Response>): Promise<T
|
||||
}
|
||||
|
||||
if (!r?.ok) {
|
||||
if (errorCallback) {
|
||||
return errorCallback(r)
|
||||
}
|
||||
const text = await r.text();
|
||||
throw new NetworkCommunicationError(`Wrong response code: ${r.status}. ${text}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await r.json();
|
||||
if (successCallback) {
|
||||
return successCallback(result);
|
||||
}
|
||||
return result as T;
|
||||
} catch (e) {
|
||||
throw new NetworkCommunicationError(`Error while parsing json response: ${JSON.stringify(e)}`);
|
||||
|
||||
Reference in New Issue
Block a user