@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "warp-contracts",
|
"name": "warp-contracts",
|
||||||
"version": "1.4.25",
|
"version": "1.5.0-rc.0",
|
||||||
"description": "An implementation of the SmartWeave smart contract protocol.",
|
"description": "An implementation of the SmartWeave smart contract protocol.",
|
||||||
"types": "./lib/types/index.d.ts",
|
"types": "./lib/types/index.d.ts",
|
||||||
"main": "./lib/cjs/index.js",
|
"main": "./lib/cjs/index.js",
|
||||||
|
|||||||
@@ -14,171 +14,177 @@ import { WARP_TAGS } from '../../../core/KnownTags';
|
|||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
|
||||||
interface ExampleContractState {
|
interface ExampleContractState {
|
||||||
counter: number;
|
counter: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DECENTRALIZED_SEQUENCER_URLS = ['http://sequencer-0.testnet.warp.cc:1317',
|
const DECENTRALIZED_SEQUENCER_URLS = [
|
||||||
'http://sequencer-1.testnet.warp.cc:1317',
|
'http://sequencer-0.testnet.warp.cc:1317',
|
||||||
'http://sequencer-2.testnet.warp.cc:1317'];
|
'http://sequencer-1.testnet.warp.cc:1317',
|
||||||
|
'http://sequencer-2.testnet.warp.cc:1317'
|
||||||
|
];
|
||||||
const GW_URL = 'http://35.242.203.146:5666/';
|
const GW_URL = 'http://35.242.203.146:5666/';
|
||||||
|
|
||||||
describe('Testing sending of interactions to a decentralized sequencer', () => {
|
describe('Testing sending of interactions to a decentralized sequencer', () => {
|
||||||
let contractSrc: string;
|
let contractSrc: string;
|
||||||
let initialState: string;
|
let initialState: string;
|
||||||
let wallet: JWKInterface;
|
let wallet: JWKInterface;
|
||||||
let arlocal: ArLocal;
|
let arlocal: ArLocal;
|
||||||
let warp: Warp;
|
let warp: Warp;
|
||||||
let contract: Contract<ExampleContractState>;
|
let contract: Contract<ExampleContractState>;
|
||||||
let mockGwServer: Server;
|
let mockGwServer: Server;
|
||||||
let mockGwUrl: string;
|
let mockGwUrl: string;
|
||||||
let centralizedSequencerType: boolean;
|
let centralizedSequencerType: boolean;
|
||||||
let confirmAnyTx: boolean;
|
let confirmAnyTx: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* For testing purposes, operations returning the sequencer's address and registering/confirming interactions are mocked.
|
|
||||||
* Other requests are forwarded to the real Gateway.
|
|
||||||
*/
|
|
||||||
const mockGw = async () => {
|
|
||||||
mockGwServer = createServer((req, res) => {
|
|
||||||
if (req.url === '/gateway/sequencer/address') {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
urls: centralizedSequencerType ? [mockGwUrl] : DECENTRALIZED_SEQUENCER_URLS,
|
|
||||||
type: centralizedSequencerType ? 'centralized' : 'decentralized'
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
} else if (req.url === '/gateway/v2/sequencer/register') {
|
|
||||||
centralizedSequencerType = false;
|
|
||||||
res.writeHead(301, { Location: DECENTRALIZED_SEQUENCER_URLS[0] });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
} else if (req.url?.startsWith('/gateway/interactions/')) {
|
|
||||||
res.writeHead(confirmAnyTx ? 200 : 204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
hostname: new URL(GW_URL).hostname,
|
|
||||||
port: new URL(GW_URL).port,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers: req.headers
|
|
||||||
};
|
|
||||||
|
|
||||||
var proxy = request(options, (gwRes) => {
|
|
||||||
if (gwRes.statusCode) {
|
|
||||||
res.writeHead(gwRes.statusCode, gwRes.headers)
|
|
||||||
gwRes.pipe(res, {
|
|
||||||
end: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.pipe(proxy, {
|
|
||||||
end: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await new Promise<void>(resolve => {
|
|
||||||
mockGwServer.listen(() => {
|
|
||||||
const address = mockGwServer.address() as AddressInfo
|
|
||||||
mockGwUrl = `http://localhost:${address.port}`
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
LoggerFactory.INST.logLevel('debug');
|
|
||||||
const port = 1813;
|
|
||||||
arlocal = new ArLocal(port, false);
|
|
||||||
await arlocal.start();
|
|
||||||
|
|
||||||
const arweave = Arweave.init({
|
|
||||||
host: 'localhost',
|
|
||||||
port: port,
|
|
||||||
protocol: 'http'
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing purposes, operations returning the sequencer's address and registering/confirming interactions are mocked.
|
||||||
|
* Other requests are forwarded to the real Gateway.
|
||||||
|
*/
|
||||||
|
const mockGw = async () => {
|
||||||
|
mockGwServer = createServer((req, res) => {
|
||||||
|
if (req.url === '/gateway/sequencer/address') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
urls: centralizedSequencerType ? [mockGwUrl] : DECENTRALIZED_SEQUENCER_URLS,
|
||||||
|
type: centralizedSequencerType ? 'centralized' : 'decentralized'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (req.url === '/gateway/v2/sequencer/register') {
|
||||||
centralizedSequencerType = false;
|
centralizedSequencerType = false;
|
||||||
confirmAnyTx = false;
|
res.writeHead(301, { Location: DECENTRALIZED_SEQUENCER_URLS[0] });
|
||||||
await mockGw();
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url?.startsWith('/gateway/interactions/')) {
|
||||||
|
res.writeHead(confirmAnyTx ? 200 : 204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cacheOptions = {
|
var options = {
|
||||||
...defaultCacheOptions,
|
hostname: new URL(GW_URL).hostname,
|
||||||
inMemory: true
|
port: new URL(GW_URL).port,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
var proxy = request(options, (gwRes) => {
|
||||||
|
if (gwRes.statusCode) {
|
||||||
|
res.writeHead(gwRes.statusCode, gwRes.headers);
|
||||||
|
gwRes.pipe(res, {
|
||||||
|
end: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const gatewayOptions = { ...defaultWarpGwOptions, source: SourceType.WARP_SEQUENCER, confirmationStatus: { notCorrupted: true } }
|
});
|
||||||
|
|
||||||
warp = WarpFactory
|
req.pipe(proxy, {
|
||||||
.custom(arweave, cacheOptions, 'custom')
|
end: true
|
||||||
.useWarpGateway(gatewayOptions, cacheOptions)
|
});
|
||||||
.build()
|
});
|
||||||
.useGwUrl(mockGwUrl)
|
await new Promise<void>((resolve) => {
|
||||||
.use(new DeployPlugin());
|
mockGwServer.listen(() => {
|
||||||
|
const address = mockGwServer.address() as AddressInfo;
|
||||||
|
mockGwUrl = `http://localhost:${address.port}`;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
({ jwk: wallet } = await warp.generateWallet());
|
beforeAll(async () => {
|
||||||
|
LoggerFactory.INST.logLevel('debug');
|
||||||
contractSrc = fs.readFileSync(path.join(__dirname, '../data/example-contract.js'), 'utf8');
|
const port = 1813;
|
||||||
initialState = fs.readFileSync(path.join(__dirname, '../data/example-contract-state.json'), 'utf8');
|
arlocal = new ArLocal(port, false);
|
||||||
|
await arlocal.start();
|
||||||
const { contractTxId } = await warp.deploy({
|
|
||||||
wallet: new ArweaveSigner(wallet),
|
|
||||||
initState: initialState,
|
|
||||||
src: contractSrc
|
|
||||||
});
|
|
||||||
|
|
||||||
contract = warp.contract<ExampleContractState>(contractTxId).setEvaluationOptions({
|
|
||||||
sequencerUrl: mockGwUrl
|
|
||||||
});
|
|
||||||
contract.connect(wallet);
|
|
||||||
|
|
||||||
|
const arweave = Arweave.init({
|
||||||
|
host: 'localhost',
|
||||||
|
port: port,
|
||||||
|
protocol: 'http'
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
centralizedSequencerType = false;
|
||||||
await arlocal.stop();
|
confirmAnyTx = false;
|
||||||
await new Promise(resolve => {
|
await mockGw();
|
||||||
mockGwServer.close(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()
|
||||||
|
.useGwUrl(mockGwUrl)
|
||||||
|
.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
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNonceFromResult = (result: WriteInteractionResponse | null): number => {
|
contract = warp.contract<ExampleContractState>(contractTxId).setEvaluationOptions({
|
||||||
if (result) {
|
sequencerUrl: mockGwUrl
|
||||||
for (let tag of result.interactionTx.tags) {
|
});
|
||||||
if (tag.name === WARP_TAGS.SEQUENCER_NONCE) {
|
contract.connect(wallet);
|
||||||
return Number(tag.value)
|
});
|
||||||
}
|
|
||||||
}
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
mockGwServer.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
|
}
|
||||||
}
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
it('should follow the redirection returned by the centralized sequencer.', async () => {
|
it('should follow the redirection returned by the centralized sequencer.', async () => {
|
||||||
confirmAnyTx = true;
|
confirmAnyTx = true;
|
||||||
centralizedSequencerType = true;
|
centralizedSequencerType = true;
|
||||||
contract.setEvaluationOptions({
|
contract.setEvaluationOptions({
|
||||||
waitForConfirmation: true
|
waitForConfirmation: true
|
||||||
});
|
|
||||||
|
|
||||||
await contract.writeInteraction({ function: 'add' });
|
|
||||||
const result = await contract.writeInteraction({ function: 'add' });
|
|
||||||
expect(getNonceFromResult(result)).toEqual(1)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add new interactions waiting for confirmation from the gateway', async () => {
|
await contract.writeInteraction({ function: 'add' });
|
||||||
contract.setEvaluationOptions({ waitForConfirmation: true })
|
const result = await contract.writeInteraction({ function: 'add' });
|
||||||
setTimeout(() => confirmAnyTx = true, 2000);
|
expect(getNonceFromResult(result)).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
await contract.writeInteraction({ function: 'add' });
|
it('should add new interactions waiting for confirmation from the gateway', async () => {
|
||||||
const result = await contract.writeInteraction({ function: 'add' });
|
contract.setEvaluationOptions({ waitForConfirmation: true });
|
||||||
expect(getNonceFromResult(result)).toEqual(3)
|
setTimeout(() => (confirmAnyTx = true), 2000);
|
||||||
});
|
|
||||||
|
|
||||||
it('should add new interactions without waiting for confirmation from the gateway', async () => {
|
await contract.writeInteraction({ function: 'add' });
|
||||||
contract.setEvaluationOptions({ waitForConfirmation: false })
|
const result = await contract.writeInteraction({ function: 'add' });
|
||||||
|
expect(getNonceFromResult(result)).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
await contract.writeInteraction({ function: 'add' });
|
it('should add new interactions without waiting for confirmation from the gateway', async () => {
|
||||||
const result = await contract.writeInteraction({ function: 'add' });
|
contract.setEvaluationOptions({ waitForConfirmation: false });
|
||||||
expect(getNonceFromResult(result)).toEqual(5)
|
|
||||||
});
|
await contract.writeInteraction({ function: 'add' });
|
||||||
|
const result = await contract.writeInteraction({ function: 'add' });
|
||||||
|
expect(getNonceFromResult(result)).toEqual(5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,33 +13,38 @@ const SEQUENCER_URL = 'http://sequencer-0.testnet.warp.cc:1317';
|
|||||||
const GW_URL = 'https://gw-testnet.warp.cc';
|
const GW_URL = 'https://gw-testnet.warp.cc';
|
||||||
|
|
||||||
describe('Testing a decentralized sequencer client', () => {
|
describe('Testing a decentralized sequencer client', () => {
|
||||||
|
|
||||||
const createClient = (): SequencerClient => {
|
const createClient = (): SequencerClient => {
|
||||||
const warpFetchWrapper = new WarpFetchWrapper(WarpFactory.forLocal())
|
const warpFetchWrapper = new WarpFetchWrapper(WarpFactory.forLocal());
|
||||||
return new DecentralizedSequencerClient(SEQUENCER_URL, GW_URL, warpFetchWrapper);
|
return new DecentralizedSequencerClient(SEQUENCER_URL, GW_URL, warpFetchWrapper);
|
||||||
}
|
};
|
||||||
|
|
||||||
const createSignature = async (): Promise<Signature> => {
|
const createSignature = async (): Promise<Signature> => {
|
||||||
const wallet = await Arweave.crypto.generateJWK();
|
const wallet = await Arweave.crypto.generateJWK();
|
||||||
const signer = new ArweaveSigner(wallet);
|
const signer = new ArweaveSigner(wallet);
|
||||||
return new Signature(WarpFactory.forLocal(), signer)
|
return new Signature(WarpFactory.forLocal(), signer);
|
||||||
}
|
};
|
||||||
|
|
||||||
const createDataItem = async (signature: Signature, nonce: number, addNonceTag = true, addContractTag = true, signDataItem = true): Promise<DataItem> => {
|
const createDataItem = async (
|
||||||
|
signature: Signature,
|
||||||
|
nonce: number,
|
||||||
|
addNonceTag = true,
|
||||||
|
addContractTag = true,
|
||||||
|
signDataItem = true
|
||||||
|
): Promise<DataItem> => {
|
||||||
const signer = signature.bundlerSigner;
|
const signer = signature.bundlerSigner;
|
||||||
const tags: Tag[] = [];
|
const tags: Tag[] = [];
|
||||||
if (addNonceTag) {
|
if (addNonceTag) {
|
||||||
tags.push(new Tag(WARP_TAGS.SEQUENCER_NONCE, String(nonce)));
|
tags.push(new Tag(WARP_TAGS.SEQUENCER_NONCE, String(nonce)));
|
||||||
}
|
}
|
||||||
if (addContractTag) {
|
if (addContractTag) {
|
||||||
tags.push(new Tag(SMART_WEAVE_TAGS.CONTRACT_TX_ID, "unit test contract"));
|
tags.push(new Tag(SMART_WEAVE_TAGS.CONTRACT_TX_ID, 'unit test contract'));
|
||||||
}
|
}
|
||||||
const dataItem = createData('some data', signer, { tags });
|
const dataItem = createData('some data', signer, { tags });
|
||||||
if (signDataItem) {
|
if (signDataItem) {
|
||||||
await dataItem.sign(signer);
|
await dataItem.sign(signer);
|
||||||
}
|
}
|
||||||
return dataItem;
|
return dataItem;
|
||||||
}
|
};
|
||||||
|
|
||||||
it('should return consecutive nonces for a given signature', async () => {
|
it('should return consecutive nonces for a given signature', async () => {
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
@@ -56,9 +61,9 @@ describe('Testing a decentralized sequencer client', () => {
|
|||||||
const signature = await createSignature();
|
const signature = await createSignature();
|
||||||
const dataItem = await createDataItem(signature, 13);
|
const dataItem = await createDataItem(signature, 13);
|
||||||
|
|
||||||
expect(client.sendDataItem(dataItem, false))
|
expect(client.sendDataItem(dataItem, false)).rejects.toThrowError(
|
||||||
.rejects
|
'account sequence mismatch, expected 0, got 13: incorrect account sequence'
|
||||||
.toThrowError('account sequence mismatch, expected 0, got 13: incorrect account sequence');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject a data item without nonce', async () => {
|
it('should reject a data item without nonce', async () => {
|
||||||
@@ -66,9 +71,7 @@ describe('Testing a decentralized sequencer client', () => {
|
|||||||
const signature = await createSignature();
|
const signature = await createSignature();
|
||||||
const dataItem = await createDataItem(signature, 0, false);
|
const dataItem = await createDataItem(signature, 0, false);
|
||||||
|
|
||||||
expect(client.sendDataItem(dataItem, true))
|
expect(client.sendDataItem(dataItem, true)).rejects.toThrowError('no sequencer nonce tag');
|
||||||
.rejects
|
|
||||||
.toThrowError('no sequencer nonce tag');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject a data item without contract', async () => {
|
it('should reject a data item without contract', async () => {
|
||||||
@@ -76,9 +79,7 @@ describe('Testing a decentralized sequencer client', () => {
|
|||||||
const signature = await createSignature();
|
const signature = await createSignature();
|
||||||
const dataItem = await createDataItem(signature, 0, true, false);
|
const dataItem = await createDataItem(signature, 0, true, false);
|
||||||
|
|
||||||
expect(client.sendDataItem(dataItem, true))
|
expect(client.sendDataItem(dataItem, true)).rejects.toThrowError('no contract tag');
|
||||||
.rejects
|
|
||||||
.toThrowError('no contract tag');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an unsigned data item', async () => {
|
it('should reject an unsigned data item', async () => {
|
||||||
@@ -86,9 +87,7 @@ describe('Testing a decentralized sequencer client', () => {
|
|||||||
const signature = await createSignature();
|
const signature = await createSignature();
|
||||||
const dataItem = await createDataItem(signature, 0, true, true, false);
|
const dataItem = await createDataItem(signature, 0, true, true, false);
|
||||||
|
|
||||||
expect(client.sendDataItem(dataItem, true))
|
expect(client.sendDataItem(dataItem, true)).rejects.toThrowError('data item verification error');
|
||||||
.rejects
|
|
||||||
.toThrowError('data item verification error');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an unconfirmed result', async () => {
|
it('should return an unconfirmed result', async () => {
|
||||||
|
|||||||
@@ -166,9 +166,9 @@ export class SmartWeaveGlobal {
|
|||||||
throw new Error(`Integer max value must be in the range [1, ${Number.MAX_SAFE_INTEGER}]`);
|
throw new Error(`Integer max value must be in the range [1, ${Number.MAX_SAFE_INTEGER}]`);
|
||||||
}
|
}
|
||||||
const base64 = this._activeTx.random.replace(/-/g, '+').replace(/_/g, '/');
|
const base64 = this._activeTx.random.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
const array = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
const array = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||||
const bigInt = Buffer.from(array).readBigUInt64BE();
|
const bigInt = Buffer.from(array).readBigUInt64BE();
|
||||||
const result = (bigInt % BigInt(maxValue)) + BigInt(1)
|
const result = (bigInt % BigInt(maxValue)) + BigInt(1);
|
||||||
return Number(result);
|
return Number(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user