feat: whitelist contract sources (#449)
* feat: whitelist contract sources * feat: sort interactions loaded from Warp Gateway
This commit is contained in:
119
src/__tests__/integration/basic/whitelist-sources-evolve.test.ts
Normal file
119
src/__tests__/integration/basic/whitelist-sources-evolve.test.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
import path from 'path';
|
||||||
|
import { PstState, PstContract } from '../../../contract/PstContract';
|
||||||
|
import { Warp } from '../../../core/Warp';
|
||||||
|
import { WarpFactory } from '../../../core/WarpFactory';
|
||||||
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
|
|
||||||
|
describe('Testing sources whitelisting in nested contracts (evolve)', () => {
|
||||||
|
let contractSrc: string;
|
||||||
|
|
||||||
|
let wallet: JWKInterface;
|
||||||
|
let walletAddress: string;
|
||||||
|
|
||||||
|
let initialState: PstState;
|
||||||
|
|
||||||
|
let arweave: Arweave;
|
||||||
|
let arlocal: ArLocal;
|
||||||
|
let warp: Warp;
|
||||||
|
let pst: PstContract;
|
||||||
|
let contractTxId: string;
|
||||||
|
let srcTxId: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
arlocal = new ArLocal(1903, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('error');
|
||||||
|
warp = WarpFactory.forLocal(1903).use(new DeployPlugin());
|
||||||
|
|
||||||
|
({ arweave } = warp);
|
||||||
|
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||||
|
|
||||||
|
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
|
||||||
|
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
...stateFromFile,
|
||||||
|
...{
|
||||||
|
owner: walletAddress,
|
||||||
|
balances: {
|
||||||
|
...stateFromFile.balances,
|
||||||
|
[walletAddress]: 555669
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
({ contractTxId, srcTxId } = await warp.deploy({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
src: contractSrc
|
||||||
|
}));
|
||||||
|
pst = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
whitelistSources: [srcTxId]
|
||||||
|
}) as PstContract;
|
||||||
|
pst.connect(wallet);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read pst state and balance data', async () => {
|
||||||
|
expect(await pst.currentState()).toEqual(initialState);
|
||||||
|
|
||||||
|
expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000);
|
||||||
|
expect((await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222);
|
||||||
|
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly transfer tokens', async () => {
|
||||||
|
await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555);
|
||||||
|
expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop evaluation after evolve to non-whitelisted source', async () => {
|
||||||
|
expect((await pst.currentState()).balances[walletAddress]).toEqual(555114);
|
||||||
|
|
||||||
|
const srcTx = await warp.createSource({ src: contractSrc }, wallet);
|
||||||
|
const newSrcTxId = await warp.saveSource(srcTx);
|
||||||
|
|
||||||
|
const evolveResponse = await pst.evolve(newSrcTxId);
|
||||||
|
|
||||||
|
await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
// note: should not evolve - the balance should be 555114 (the evolved version ads 555 to the balance)
|
||||||
|
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555114);
|
||||||
|
|
||||||
|
await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(pst.readState()).rejects.toThrow(
|
||||||
|
`[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${newSrcTxId}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// testcase for new warp instance
|
||||||
|
const newWarp = WarpFactory.forLocal(1903).use(new DeployPlugin());
|
||||||
|
const freshPst = newWarp.contract(contractTxId);
|
||||||
|
const freshResult = await freshPst.readState();
|
||||||
|
// note: should not evaluate at all the last interaction
|
||||||
|
expect(Object.keys(freshResult.cachedValue.validity).length).toEqual(4);
|
||||||
|
expect(Object.keys(freshResult.cachedValue.errorMessages).length).toEqual(0);
|
||||||
|
|
||||||
|
expect(freshResult.cachedValue.validity[evolveResponse.originalTxId]).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
import path from 'path';
|
||||||
|
import { PstContract, PstState } from '../../../contract/PstContract';
|
||||||
|
import { Warp } from '../../../core/Warp';
|
||||||
|
import { WarpFactory } from '../../../core/WarpFactory';
|
||||||
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
|
|
||||||
|
describe('Testing sources whitelisting in nested contracts (read)', () => {
|
||||||
|
let contractSrc: string;
|
||||||
|
let foreignSrc: string;
|
||||||
|
|
||||||
|
let wallet: JWKInterface;
|
||||||
|
let walletAddress: string;
|
||||||
|
|
||||||
|
let initialState: PstState;
|
||||||
|
|
||||||
|
let arweave: Arweave;
|
||||||
|
let arlocal: ArLocal;
|
||||||
|
let warp: Warp;
|
||||||
|
let pst, blacklistPst, whitelistPst: PstContract;
|
||||||
|
let blacklistSrcTxId: string;
|
||||||
|
let foreignWhitelistTxId, foreignBlacklistTxId: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
arlocal = new ArLocal(1901, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('debug');
|
||||||
|
warp = WarpFactory.forLocal(1901).use(new DeployPlugin());
|
||||||
|
|
||||||
|
({ arweave } = warp);
|
||||||
|
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||||
|
|
||||||
|
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
|
||||||
|
const mainSrcTx = await warp.createSource({ src: contractSrc }, wallet);
|
||||||
|
const mainSrcTxId = await warp.saveSource(mainSrcTx);
|
||||||
|
|
||||||
|
foreignSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8');
|
||||||
|
const blacklistContractSrcTx = await warp.createSource({ src: foreignSrc }, wallet);
|
||||||
|
blacklistSrcTxId = await warp.saveSource(blacklistContractSrcTx);
|
||||||
|
const whitelistSrcTx = await warp.createSource({ src: foreignSrc }, wallet);
|
||||||
|
const whitelistSrcTxId = await warp.saveSource(whitelistSrcTx);
|
||||||
|
|
||||||
|
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
...stateFromFile,
|
||||||
|
...{
|
||||||
|
owner: walletAddress,
|
||||||
|
balances: {
|
||||||
|
...stateFromFile.balances,
|
||||||
|
[walletAddress]: 555669
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { contractTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: mainSrcTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
({ contractTxId: foreignWhitelistTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: whitelistSrcTxId
|
||||||
|
}));
|
||||||
|
|
||||||
|
({ contractTxId: foreignBlacklistTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: blacklistSrcTxId
|
||||||
|
}));
|
||||||
|
|
||||||
|
pst = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
whitelistSources: [mainSrcTxId, whitelistSrcTxId]
|
||||||
|
}) as PstContract;
|
||||||
|
pst.connect(wallet);
|
||||||
|
|
||||||
|
blacklistPst = warp.pst(foreignBlacklistTxId).connect(wallet) as PstContract;
|
||||||
|
|
||||||
|
whitelistPst = warp.pst(foreignWhitelistTxId).connect(wallet) as PstContract;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly transfer tokens', async () => {
|
||||||
|
await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555);
|
||||||
|
expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly read foreign contract with whitelisted source', async () => {
|
||||||
|
await whitelistPst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
const { originalTxId } = await pst.writeInteraction({
|
||||||
|
function: 'readForeign',
|
||||||
|
contractTxId: foreignWhitelistTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect(result.cachedValue.validity[originalTxId]).toBe(true);
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop evaluation of a contract which is not in the whitelist (readContractState)', async () => {
|
||||||
|
const readBlacklistedTx = await pst.writeInteraction({
|
||||||
|
function: 'readForeign',
|
||||||
|
contractTxId: foreignBlacklistTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
|
||||||
|
expect(Object.keys(result.cachedValue.validity).length == 2);
|
||||||
|
expect(Object.keys(result.cachedValue.errorMessages).length == 2);
|
||||||
|
|
||||||
|
expect(result.cachedValue.validity[readBlacklistedTx.originalTxId]).toBe(false);
|
||||||
|
expect(result.cachedValue.errorMessages[readBlacklistedTx.originalTxId]).toMatch(
|
||||||
|
`Contract source not part of whitelisted sources list: ${blacklistSrcTxId}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// should not change from previous test
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip evaluation when foreign whitelisted contract evolves to non-whitelisted source (readContractState)', async () => {
|
||||||
|
const readWhitelistedTx = await pst.writeInteraction({
|
||||||
|
function: 'readForeign',
|
||||||
|
contractTxId: foreignWhitelistTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
await whitelistPst.evolve(blacklistSrcTxId);
|
||||||
|
|
||||||
|
await whitelistPst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
const readEvolvedBlacklistTx = await pst.writeInteraction({
|
||||||
|
function: 'readForeign',
|
||||||
|
contractTxId: foreignWhitelistTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastWrittenTx = await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect(result.cachedValue.validity[readWhitelistedTx.originalTxId]).toBe(true);
|
||||||
|
expect(result.cachedValue.validity[readEvolvedBlacklistTx.originalTxId]).toBe(false);
|
||||||
|
|
||||||
|
// note: the transactions after foreign read from evolved to unsafe contract should be processed normally
|
||||||
|
expect(result.cachedValue.validity[lastWrittenTx.originalTxId]).toBe(true);
|
||||||
|
|
||||||
|
// should be incremented by one - only the first read from this testcase should be successful
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
import path from 'path';
|
||||||
|
import { PstContract, PstState } from '../../../contract/PstContract';
|
||||||
|
import { Warp } from '../../../core/Warp';
|
||||||
|
import { WarpFactory } from '../../../core/WarpFactory';
|
||||||
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
|
|
||||||
|
describe('Testing sources whitelisting in nested contracts (write)', () => {
|
||||||
|
let contractSrc, foreignContractSrc: string;
|
||||||
|
|
||||||
|
let wallet: JWKInterface;
|
||||||
|
let walletAddress: string;
|
||||||
|
|
||||||
|
let initialState: PstState;
|
||||||
|
|
||||||
|
let arweave: Arweave;
|
||||||
|
let arlocal: ArLocal;
|
||||||
|
let warp, warpBlacklisted: Warp;
|
||||||
|
let pst, foreignWhitelistedPst, foreignBlacklistedPst: PstContract;
|
||||||
|
let contractTxId,
|
||||||
|
foreignBlacklistedContractTxId,
|
||||||
|
foreignBlacklistedSrcTxId,
|
||||||
|
foreignWhitelistedContractTxId,
|
||||||
|
foreignWhitelistedSrcTxId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
arlocal = new ArLocal(1902, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('error');
|
||||||
|
warp = WarpFactory.forLocal(1902).use(new DeployPlugin());
|
||||||
|
warpBlacklisted = WarpFactory.forLocal(1902).use(new DeployPlugin());
|
||||||
|
|
||||||
|
({ arweave } = warp);
|
||||||
|
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||||
|
|
||||||
|
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
|
||||||
|
const srcTx = await warp.createSource({ src: contractSrc }, wallet);
|
||||||
|
const srcTxId = await warp.saveSource(srcTx);
|
||||||
|
|
||||||
|
foreignContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8');
|
||||||
|
const foreignBlacklistContractSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet);
|
||||||
|
foreignBlacklistedSrcTxId = await warp.saveSource(foreignBlacklistContractSrcTx);
|
||||||
|
const foreignWhitelistSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet);
|
||||||
|
foreignWhitelistedSrcTxId = await warp.saveSource(foreignWhitelistSrcTx);
|
||||||
|
|
||||||
|
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
...stateFromFile,
|
||||||
|
...{
|
||||||
|
owner: walletAddress,
|
||||||
|
balances: {
|
||||||
|
...stateFromFile.balances,
|
||||||
|
[walletAddress]: 555669
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
({ contractTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId
|
||||||
|
}));
|
||||||
|
|
||||||
|
({ contractTxId: foreignBlacklistedContractTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: foreignBlacklistedSrcTxId
|
||||||
|
}));
|
||||||
|
|
||||||
|
({ contractTxId: foreignWhitelistedContractTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: foreignWhitelistedSrcTxId
|
||||||
|
}));
|
||||||
|
|
||||||
|
pst = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
internalWrites: true,
|
||||||
|
whitelistSources: [srcTxId, foreignWhitelistedSrcTxId]
|
||||||
|
}) as PstContract;
|
||||||
|
pst.connect(wallet);
|
||||||
|
|
||||||
|
foreignWhitelistedPst = warp
|
||||||
|
.pst(foreignWhitelistedContractTxId)
|
||||||
|
.setEvaluationOptions({
|
||||||
|
internalWrites: true
|
||||||
|
})
|
||||||
|
.connect(wallet) as PstContract;
|
||||||
|
|
||||||
|
foreignBlacklistedPst = warpBlacklisted
|
||||||
|
.pst(foreignBlacklistedContractTxId)
|
||||||
|
.setEvaluationOptions({
|
||||||
|
internalWrites: true
|
||||||
|
})
|
||||||
|
.connect(wallet) as PstContract;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly transfer tokens', async () => {
|
||||||
|
await pst.transfer({
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 555
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555);
|
||||||
|
expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly perform write from foreign whitelisted contract', async () => {
|
||||||
|
await foreignWhitelistedPst.writeInteraction({
|
||||||
|
function: 'writeForeign',
|
||||||
|
contractTxId: contractTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block write from foreign blacklisted contract (1)', async () => {
|
||||||
|
const blacklistedWriteTx = await foreignBlacklistedPst.writeInteraction({
|
||||||
|
function: 'writeForeign',
|
||||||
|
contractTxId: contractTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy();
|
||||||
|
// should not change from previous test
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block write from foreign blacklisted contract (2)', async () => {
|
||||||
|
const blacklistedWriteTx = await foreignBlacklistedPst.writeInteraction({
|
||||||
|
function: 'writeForeign',
|
||||||
|
contractTxId: contractTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy();
|
||||||
|
// should not change from previous test
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block write from foreign whitelisted contract that evolved to blacklisted', async () => {
|
||||||
|
const srcTx = await warp.createSource({ src: foreignContractSrc }, wallet);
|
||||||
|
const nonWhitelistedSrcTxId = await warp.saveSource(srcTx);
|
||||||
|
|
||||||
|
await foreignWhitelistedPst.evolve(nonWhitelistedSrcTxId);
|
||||||
|
|
||||||
|
const blacklistedWriteTx = await foreignWhitelistedPst.writeInteraction({
|
||||||
|
function: 'writeForeign',
|
||||||
|
contractTxId: contractTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pst.readState();
|
||||||
|
expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy();
|
||||||
|
// should not change from previous test
|
||||||
|
expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/__tests__/integration/basic/whitelist-sources.test.ts
Normal file
94
src/__tests__/integration/basic/whitelist-sources.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
import path from 'path';
|
||||||
|
import { PstContract, PstState } from '../../../contract/PstContract';
|
||||||
|
import { Warp } from '../../../core/Warp';
|
||||||
|
import { WarpFactory } from '../../../core/WarpFactory';
|
||||||
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
|
|
||||||
|
describe('Testing whitelist sources in nested contracts', () => {
|
||||||
|
let contractSrc, foreignContractSrc: string;
|
||||||
|
|
||||||
|
let wallet: JWKInterface;
|
||||||
|
let walletAddress: string;
|
||||||
|
|
||||||
|
let initialState: PstState;
|
||||||
|
|
||||||
|
let arweave: Arweave;
|
||||||
|
let arlocal: ArLocal;
|
||||||
|
let warp: Warp;
|
||||||
|
let mainSrcTxId: string;
|
||||||
|
let pst, pstWhitelisted, pstBlacklisted: PstContract;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
arlocal = new ArLocal(1900, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('error');
|
||||||
|
warp = WarpFactory.forLocal(1900).use(new DeployPlugin());
|
||||||
|
|
||||||
|
({ arweave } = warp);
|
||||||
|
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||||
|
|
||||||
|
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
|
||||||
|
const mainSrcTx = await warp.createSource({ src: contractSrc }, wallet);
|
||||||
|
mainSrcTxId = await warp.saveSource(mainSrcTx);
|
||||||
|
|
||||||
|
foreignContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8');
|
||||||
|
const foreignSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet);
|
||||||
|
const foreignSrcTxId = await warp.saveSource(foreignSrcTx);
|
||||||
|
|
||||||
|
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
...stateFromFile,
|
||||||
|
...{
|
||||||
|
owner: walletAddress,
|
||||||
|
balances: {
|
||||||
|
...stateFromFile.balances,
|
||||||
|
[walletAddress]: 555669
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { contractTxId } = await warp.deployFromSourceTx({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
srcTxId: mainSrcTxId
|
||||||
|
});
|
||||||
|
|
||||||
|
pst = warp.pst(contractTxId) as PstContract;
|
||||||
|
pst.connect(wallet);
|
||||||
|
|
||||||
|
pstWhitelisted = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
whitelistSources: [mainSrcTxId]
|
||||||
|
}) as PstContract;
|
||||||
|
pstWhitelisted.connect(wallet);
|
||||||
|
|
||||||
|
pstBlacklisted = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
whitelistSources: [foreignSrcTxId]
|
||||||
|
}) as PstContract;
|
||||||
|
pstBlacklisted.connect(wallet);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow to evaluate contract by default', async () => {
|
||||||
|
expect(await pst.readState()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow to evaluate contract when src tx id is in the whitelist', async () => {
|
||||||
|
expect(await pstWhitelisted.readState()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow to evaluate contract when src tx id is in the whitelist', async () => {
|
||||||
|
await expect(pstBlacklisted.readState()).rejects.toThrowError(
|
||||||
|
`[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${mainSrcTxId}.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
73
src/__tests__/integration/data/token-pst-foreign.js
Normal file
73
src/__tests__/integration/data/token-pst-foreign.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export async function handle(state, action) {
|
||||||
|
const balances = state.balances;
|
||||||
|
const canEvolve = state.canEvolve;
|
||||||
|
const input = action.input;
|
||||||
|
const caller = action.caller;
|
||||||
|
|
||||||
|
if (input.function === 'transfer') {
|
||||||
|
const target = input.target;
|
||||||
|
const qty = input.qty;
|
||||||
|
|
||||||
|
if (!Number.isInteger(qty)) {
|
||||||
|
throw new ContractError('Invalid value for "qty". Must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new ContractError('No target specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qty <= 0 || caller === target) {
|
||||||
|
throw new ContractError('Invalid token transfer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balances[caller] < qty) {
|
||||||
|
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower the token balance of the caller
|
||||||
|
balances[caller] -= qty;
|
||||||
|
if (target in balances) {
|
||||||
|
// Wallet already exists in state, add new tokens
|
||||||
|
balances[target] += qty;
|
||||||
|
} else {
|
||||||
|
// Wallet is new, set starting balance
|
||||||
|
balances[target] = qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'balance') {
|
||||||
|
const target = input.target;
|
||||||
|
const ticker = state.ticker;
|
||||||
|
|
||||||
|
if (typeof target !== 'string') {
|
||||||
|
throw new ContractError('Must specify target to get balance for');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof balances[target] !== 'number') {
|
||||||
|
throw new ContractError('Cannot get balance, target does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: { target, ticker, balance: balances[target] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'evolve' && canEvolve) {
|
||||||
|
if (state.owner !== caller) {
|
||||||
|
throw new ContractError('Only the owner can evolve a contract.');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.evolve = input.value;
|
||||||
|
|
||||||
|
return { state };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'writeForeign') {
|
||||||
|
const result = await SmartWeave.contracts.write(input.contractTxId, {
|
||||||
|
function: "callFromForeign"
|
||||||
|
});
|
||||||
|
return {state};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
|
||||||
|
}
|
||||||
@@ -30,7 +30,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
contract.setEvaluationOptions({
|
contract.setEvaluationOptions({
|
||||||
@@ -67,7 +68,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const contract2 = warp.contract(null).setEvaluationOptions({
|
const contract2 = warp.contract(null).setEvaluationOptions({
|
||||||
@@ -99,7 +101,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -128,7 +131,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -157,7 +161,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -186,7 +191,8 @@ describe('Evaluation options evaluator', () => {
|
|||||||
useKVStorage: false,
|
useKVStorage: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
useConstructor: false,
|
useConstructor: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||||
|
whitelistSources: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const contract3 = warp.contract(null).setEvaluationOptions({
|
const contract3 = warp.contract(null).setEvaluationOptions({
|
||||||
|
|||||||
@@ -19,54 +19,46 @@ const responseData = {
|
|||||||
},
|
},
|
||||||
interactions: [
|
interactions: [
|
||||||
{
|
{
|
||||||
status: 'confirmed',
|
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||||
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
|
fee: {
|
||||||
confirmations: '172044,172044,172044',
|
winston: '48173811033'
|
||||||
interaction: {
|
},
|
||||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
tags: [],
|
||||||
fee: {
|
block: {
|
||||||
winston: '48173811033'
|
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||||
},
|
height: 655393,
|
||||||
tags: [],
|
timestamp: 1617060107
|
||||||
block: {
|
},
|
||||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
owner: {
|
||||||
height: 655393,
|
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||||
timestamp: 1617060107
|
},
|
||||||
},
|
parent: null,
|
||||||
owner: {
|
quantity: {
|
||||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
winston: '0'
|
||||||
},
|
},
|
||||||
parent: null,
|
recipient: '',
|
||||||
quantity: {
|
sortKey: '000000645844,0000000000000,4e49e10a3c76445b00501b704e9caab118c14ad56694a16e7e4c43c2c142e006'
|
||||||
winston: '0'
|
|
||||||
},
|
|
||||||
recipient: ''
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 'confirmed',
|
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||||
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
|
fee: {
|
||||||
confirmations: '172044,172044,172044',
|
winston: '48173811033'
|
||||||
interaction: {
|
},
|
||||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
tags: [],
|
||||||
fee: {
|
block: {
|
||||||
winston: '48173811033'
|
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||||
},
|
height: 655393,
|
||||||
tags: [],
|
timestamp: 1617060107
|
||||||
block: {
|
},
|
||||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
owner: {
|
||||||
height: 655393,
|
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||||
timestamp: 1617060107
|
},
|
||||||
},
|
parent: null,
|
||||||
owner: {
|
quantity: {
|
||||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
winston: '0'
|
||||||
},
|
},
|
||||||
parent: null,
|
recipient: '',
|
||||||
quantity: {
|
sortKey: '000000662481,0000000000000,82ef246cdc8be74447260bcbf44c21239f8ee7a36af51b29c3dc714bcefb0509'
|
||||||
winston: '0'
|
|
||||||
},
|
|
||||||
recipient: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ export class EvaluationOptionsEvaluator {
|
|||||||
remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'],
|
remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'],
|
||||||
remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'],
|
remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'],
|
||||||
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'],
|
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'],
|
||||||
useConstructor: (foreignOptions) => foreignOptions['useConstructor']
|
useConstructor: (foreignOptions) => foreignOptions['useConstructor'],
|
||||||
|
whitelistSources: () => this.rootOptions['whitelistSources']
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [
|
private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
|||||||
remoteStateSyncSource = 'https://dre-1.warp.cc/contract';
|
remoteStateSyncSource = 'https://dre-1.warp.cc/contract';
|
||||||
|
|
||||||
useConstructor = false;
|
useConstructor = false;
|
||||||
|
|
||||||
|
whitelistSources = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
|
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
|
||||||
@@ -238,4 +240,6 @@ export interface EvaluationOptions {
|
|||||||
|
|
||||||
// remote source for fetching most recent contract state, only applicable if remoteStateSyncEnabled is set to true
|
// remote source for fetching most recent contract state, only applicable if remoteStateSyncEnabled is set to true
|
||||||
remoteStateSyncSource: string;
|
remoteStateSyncSource: string;
|
||||||
|
|
||||||
|
whitelistSources: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExe
|
|||||||
import { TagsParser } from './TagsParser';
|
import { TagsParser } from './TagsParser';
|
||||||
import { VrfPluginFunctions } from '../../WarpPlugin';
|
import { VrfPluginFunctions } from '../../WarpPlugin';
|
||||||
import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache';
|
import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache';
|
||||||
|
import { KnownErrors } from './handler/JsHandlerApi';
|
||||||
|
|
||||||
type EvaluationProgressInput = {
|
type EvaluationProgressInput = {
|
||||||
contractTxId: string;
|
contractTxId: string;
|
||||||
@@ -155,8 +156,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ppe: not sure why we're not handling all ContractErrors here...
|
// ppe: not sure why we're not handling all ContractErrors here...
|
||||||
if (
|
if (
|
||||||
e.name == 'ContractError' &&
|
(e.name == KnownErrors.ContractError &&
|
||||||
(e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip')
|
(e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip')) ||
|
||||||
|
e.name == KnownErrors.NonWhitelistedSourceError
|
||||||
) {
|
) {
|
||||||
this.logger.warn(`Skipping contract in internal write, reason ${e.subtype}`);
|
this.logger.warn(`Skipping contract in internal write, reason ${e.subtype}`);
|
||||||
errorMessages[missingInteraction.id] = e;
|
errorMessages[missingInteraction.id] = e;
|
||||||
@@ -282,7 +284,10 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
executionContext = await modify<State>(currentState, executionContext);
|
executionContext = await modify<State>(currentState, executionContext);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
|
if (
|
||||||
|
(e.name == KnownErrors.ContractError && e.subtype == 'unsafeClientSkip') ||
|
||||||
|
e.name == KnownErrors.NonWhitelistedSourceError
|
||||||
|
) {
|
||||||
validity[missingInteraction.id] = false;
|
validity[missingInteraction.id] = false;
|
||||||
errorMessages[missingInteraction.id] = e.message;
|
errorMessages[missingInteraction.id] = e.message;
|
||||||
shouldBreakAfterEvolve = true;
|
shouldBreakAfterEvolve = true;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Benchmark } from '../../../logging/Benchmark';
|
|||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import { ExecutorFactory } from '../ExecutorFactory';
|
import { ExecutorFactory } from '../ExecutorFactory';
|
||||||
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
|
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
|
||||||
import { JsHandlerApi } from './handler/JsHandlerApi';
|
import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi';
|
||||||
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
||||||
import { normalizeContractSource } from './normalize-source';
|
import { normalizeContractSource } from './normalize-source';
|
||||||
import { Warp } from '../../Warp';
|
import { Warp } from '../../Warp';
|
||||||
@@ -25,7 +25,14 @@ const BigNumber = require('bignumber.js');
|
|||||||
export class ContractError<T> extends Error {
|
export class ContractError<T> extends Error {
|
||||||
constructor(readonly error: T, readonly subtype?: string) {
|
constructor(readonly error: T, readonly subtype?: string) {
|
||||||
super(error.toString());
|
super(error.toString());
|
||||||
this.name = 'ContractError';
|
this.name = KnownErrors.ContractError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NonWhitelistedSourceError<T> extends Error {
|
||||||
|
constructor(readonly error: T) {
|
||||||
|
super(error.toString());
|
||||||
|
this.name = KnownErrors.NonWhitelistedSourceError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +52,18 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
interactionState: InteractionState
|
interactionState: InteractionState
|
||||||
): Promise<HandlerApi<State>> {
|
): Promise<HandlerApi<State>> {
|
||||||
if (warp.hasPlugin('contract-blacklist')) {
|
if (warp.hasPlugin('contract-blacklist')) {
|
||||||
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('contract-blacklist');
|
await this.blacklistContracts<State>(warp, contractDefinition);
|
||||||
let blacklisted = false;
|
|
||||||
try {
|
|
||||||
blacklisted = await blacklistPlugin.process(contractDefinition.txId);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
if (blacklisted == true) {
|
|
||||||
throw new ContractError(
|
|
||||||
`[SkipUnsafeError] Skipping evaluation of the blacklisted contract ${contractDefinition.txId}.`,
|
|
||||||
`blacklistedSkip`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
evaluationOptions.whitelistSources.length > 0 &&
|
||||||
|
!evaluationOptions.whitelistSources.includes(contractDefinition.srcTxId)
|
||||||
|
) {
|
||||||
|
throw new NonWhitelistedSourceError(
|
||||||
|
`[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${contractDefinition.srcTxId}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let kvStorage = null;
|
let kvStorage = null;
|
||||||
|
|
||||||
if (evaluationOptions.useKVStorage) {
|
if (evaluationOptions.useKVStorage) {
|
||||||
@@ -213,6 +218,22 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async blacklistContracts<State>(warp: Warp, contractDefinition: ContractDefinition<State>) {
|
||||||
|
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('contract-blacklist');
|
||||||
|
let blacklisted = false;
|
||||||
|
try {
|
||||||
|
blacklisted = await blacklistPlugin.process(contractDefinition.txId);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e);
|
||||||
|
}
|
||||||
|
if (blacklisted == true) {
|
||||||
|
throw new ContractError(
|
||||||
|
`[SkipUnsafeError] Skipping evaluation of the blacklisted contract ${contractDefinition.txId}.`,
|
||||||
|
`blacklistedSkip`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateResponse(wasmBinary: Buffer): Response {
|
function generateResponse(wasmBinary: Buffer): Response {
|
||||||
|
|||||||
@@ -143,7 +143,12 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
const lastErrorMessage = stateWithValidity?.cachedValue?.errorMessages[lastErrorKey];
|
const lastErrorMessage = stateWithValidity?.cachedValue?.errorMessages[lastErrorKey];
|
||||||
// don't judge me..
|
// don't judge me..
|
||||||
// FIXME: also - '?' is stinky...
|
// FIXME: also - '?' is stinky...
|
||||||
if (lastErrorMessage?.startsWith('[SkipUnsafeError]')) {
|
if (
|
||||||
|
lastErrorMessage &&
|
||||||
|
lastErrorMessage.startsWith &&
|
||||||
|
(lastErrorMessage.startsWith('[SkipUnsafeError]') ||
|
||||||
|
lastErrorMessage.startsWith('[NonWhitelistedSourceError]'))
|
||||||
|
) {
|
||||||
throw new ContractError(lastErrorMessage);
|
throw new ContractError(lastErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const throwErrorWithName = (name: string, message: string) => {
|
|||||||
export enum KnownErrors {
|
export enum KnownErrors {
|
||||||
ContractError = 'ContractError',
|
ContractError = 'ContractError',
|
||||||
ConstructorError = 'ConstructorError',
|
ConstructorError = 'ConstructorError',
|
||||||
NetworkCommunicationError = 'NetworkCommunicationError'
|
NetworkCommunicationError = 'NetworkCommunicationError',
|
||||||
|
NonWhitelistedSourceError = 'NonWhitelistedSourceError'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||||
@@ -186,6 +187,13 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
// any network-based error should result in immediately stop contract evaluation
|
// any network-based error should result in immediately stop contract evaluation
|
||||||
case KnownErrors.NetworkCommunicationError:
|
case KnownErrors.NetworkCommunicationError:
|
||||||
throw err;
|
throw err;
|
||||||
|
case KnownErrors.NonWhitelistedSourceError:
|
||||||
|
return {
|
||||||
|
type: 'error' as const,
|
||||||
|
errorMessage: err.message,
|
||||||
|
state: state,
|
||||||
|
result: null
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
type: 'exception' as const,
|
type: 'exception' as const,
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { ContractDefinition } from '../../../../core/ContractDefinition';
|
|||||||
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
||||||
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
||||||
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
|
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
|
||||||
import { ContractError, ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
import {
|
||||||
|
ContractError,
|
||||||
|
ContractInteraction,
|
||||||
|
InteractionData,
|
||||||
|
InteractionResult,
|
||||||
|
NonWhitelistedSourceError
|
||||||
|
} from '../HandlerExecutorFactory';
|
||||||
import { AbstractContractHandler } from './AbstractContractHandler';
|
import { AbstractContractHandler } from './AbstractContractHandler';
|
||||||
import { NetworkCommunicationError } from "../../../../utils/utils";
|
import { NetworkCommunicationError } from "../../../../utils/utils";
|
||||||
|
|
||||||
@@ -58,7 +64,7 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
state: currentResult.state,
|
state: currentResult.state,
|
||||||
result: null
|
result: null
|
||||||
};
|
};
|
||||||
if (e instanceof ContractError) {
|
if (e instanceof ContractError || e instanceof NonWhitelistedSourceError) {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
error: e.error,
|
error: e.error,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ExecutionContext } from '../core/ExecutionContext';
|
|||||||
import { ExecutionContextModifier } from '../core/ExecutionContextModifier';
|
import { ExecutionContextModifier } from '../core/ExecutionContextModifier';
|
||||||
import { SmartWeaveError, SmartWeaveErrorType } from '../legacy/errors';
|
import { SmartWeaveError, SmartWeaveErrorType } from '../legacy/errors';
|
||||||
import { HandlerApi } from '../core/modules/impl/HandlerExecutorFactory';
|
import { HandlerApi } from '../core/modules/impl/HandlerExecutorFactory';
|
||||||
|
import { KnownErrors } from '../core/modules/impl/handler/JsHandlerApi';
|
||||||
|
|
||||||
function isEvolveCompatible(state: unknown): state is EvolveState {
|
function isEvolveCompatible(state: unknown): state is EvolveState {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -58,7 +59,10 @@ export class Evolve implements ExecutionContextModifier {
|
|||||||
|
|
||||||
return executionContext;
|
return executionContext;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name === 'ContractError' && e.subtype === 'unsafeClientSkip') {
|
if (
|
||||||
|
(e.name === KnownErrors.ContractError && e.subtype === 'unsafeClientSkip') ||
|
||||||
|
e.name == KnownErrors.NonWhitelistedSourceError
|
||||||
|
) {
|
||||||
throw e;
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {
|
throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {
|
||||||
|
|||||||
Reference in New Issue
Block a user