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,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||
whitelistSources: []
|
||||
});
|
||||
|
||||
contract.setEvaluationOptions({
|
||||
@@ -67,7 +68,8 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
waitForConfirmation: 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({
|
||||
@@ -99,7 +101,8 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||
whitelistSources: []
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -128,7 +131,8 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||
whitelistSources: []
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -157,7 +161,8 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
|
||||
whitelistSources: []
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -186,7 +191,8 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
waitForConfirmation: 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({
|
||||
|
||||
@@ -19,54 +19,46 @@ const responseData = {
|
||||
},
|
||||
interactions: [
|
||||
{
|
||||
status: 'confirmed',
|
||||
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
|
||||
confirmations: '172044,172044,172044',
|
||||
interaction: {
|
||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||
fee: {
|
||||
winston: '48173811033'
|
||||
},
|
||||
tags: [],
|
||||
block: {
|
||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||
height: 655393,
|
||||
timestamp: 1617060107
|
||||
},
|
||||
owner: {
|
||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||
},
|
||||
parent: null,
|
||||
quantity: {
|
||||
winston: '0'
|
||||
},
|
||||
recipient: ''
|
||||
}
|
||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||
fee: {
|
||||
winston: '48173811033'
|
||||
},
|
||||
tags: [],
|
||||
block: {
|
||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||
height: 655393,
|
||||
timestamp: 1617060107
|
||||
},
|
||||
owner: {
|
||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||
},
|
||||
parent: null,
|
||||
quantity: {
|
||||
winston: '0'
|
||||
},
|
||||
recipient: '',
|
||||
sortKey: '000000645844,0000000000000,4e49e10a3c76445b00501b704e9caab118c14ad56694a16e7e4c43c2c142e006'
|
||||
},
|
||||
{
|
||||
status: 'confirmed',
|
||||
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
|
||||
confirmations: '172044,172044,172044',
|
||||
interaction: {
|
||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||
fee: {
|
||||
winston: '48173811033'
|
||||
},
|
||||
tags: [],
|
||||
block: {
|
||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||
height: 655393,
|
||||
timestamp: 1617060107
|
||||
},
|
||||
owner: {
|
||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||
},
|
||||
parent: null,
|
||||
quantity: {
|
||||
winston: '0'
|
||||
},
|
||||
recipient: ''
|
||||
}
|
||||
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
|
||||
fee: {
|
||||
winston: '48173811033'
|
||||
},
|
||||
tags: [],
|
||||
block: {
|
||||
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
|
||||
height: 655393,
|
||||
timestamp: 1617060107
|
||||
},
|
||||
owner: {
|
||||
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
|
||||
},
|
||||
parent: null,
|
||||
quantity: {
|
||||
winston: '0'
|
||||
},
|
||||
recipient: '',
|
||||
sortKey: '000000662481,0000000000000,82ef246cdc8be74447260bcbf44c21239f8ee7a36af51b29c3dc714bcefb0509'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -106,7 +106,8 @@ export class EvaluationOptionsEvaluator {
|
||||
remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'],
|
||||
remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'],
|
||||
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'],
|
||||
useConstructor: (foreignOptions) => foreignOptions['useConstructor']
|
||||
useConstructor: (foreignOptions) => foreignOptions['useConstructor'],
|
||||
whitelistSources: () => this.rootOptions['whitelistSources']
|
||||
};
|
||||
|
||||
private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [
|
||||
|
||||
@@ -150,6 +150,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||
remoteStateSyncSource = 'https://dre-1.warp.cc/contract';
|
||||
|
||||
useConstructor = false;
|
||||
|
||||
whitelistSources = [];
|
||||
}
|
||||
|
||||
// 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
|
||||
remoteStateSyncSource: string;
|
||||
|
||||
whitelistSources: string[];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExe
|
||||
import { TagsParser } from './TagsParser';
|
||||
import { VrfPluginFunctions } from '../../WarpPlugin';
|
||||
import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache';
|
||||
import { KnownErrors } from './handler/JsHandlerApi';
|
||||
|
||||
type EvaluationProgressInput = {
|
||||
contractTxId: string;
|
||||
@@ -155,8 +156,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
} catch (e) {
|
||||
// ppe: not sure why we're not handling all ContractErrors here...
|
||||
if (
|
||||
e.name == 'ContractError' &&
|
||||
(e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip')
|
||||
(e.name == KnownErrors.ContractError &&
|
||||
(e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip')) ||
|
||||
e.name == KnownErrors.NonWhitelistedSourceError
|
||||
) {
|
||||
this.logger.warn(`Skipping contract in internal write, reason ${e.subtype}`);
|
||||
errorMessages[missingInteraction.id] = e;
|
||||
@@ -282,7 +284,10 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
executionContext = await modify<State>(currentState, executionContext);
|
||||
}
|
||||
} 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;
|
||||
errorMessages[missingInteraction.id] = e.message;
|
||||
shouldBreakAfterEvolve = true;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Benchmark } from '../../../logging/Benchmark';
|
||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||
import { ExecutorFactory } from '../ExecutorFactory';
|
||||
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
|
||||
import { JsHandlerApi } from './handler/JsHandlerApi';
|
||||
import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi';
|
||||
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
||||
import { normalizeContractSource } from './normalize-source';
|
||||
import { Warp } from '../../Warp';
|
||||
@@ -25,7 +25,14 @@ const BigNumber = require('bignumber.js');
|
||||
export class ContractError<T> extends Error {
|
||||
constructor(readonly error: T, readonly subtype?: string) {
|
||||
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
|
||||
): Promise<HandlerApi<State>> {
|
||||
if (warp.hasPlugin('contract-blacklist')) {
|
||||
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`
|
||||
);
|
||||
}
|
||||
await this.blacklistContracts<State>(warp, contractDefinition);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -143,7 +143,12 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
const lastErrorMessage = stateWithValidity?.cachedValue?.errorMessages[lastErrorKey];
|
||||
// don't judge me..
|
||||
// FIXME: also - '?' is stinky...
|
||||
if (lastErrorMessage?.startsWith('[SkipUnsafeError]')) {
|
||||
if (
|
||||
lastErrorMessage &&
|
||||
lastErrorMessage.startsWith &&
|
||||
(lastErrorMessage.startsWith('[SkipUnsafeError]') ||
|
||||
lastErrorMessage.startsWith('[NonWhitelistedSourceError]'))
|
||||
) {
|
||||
throw new ContractError(lastErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ const throwErrorWithName = (name: string, message: string) => {
|
||||
export enum KnownErrors {
|
||||
ContractError = 'ContractError',
|
||||
ConstructorError = 'ConstructorError',
|
||||
NetworkCommunicationError = 'NetworkCommunicationError'
|
||||
NetworkCommunicationError = 'NetworkCommunicationError',
|
||||
NonWhitelistedSourceError = 'NonWhitelistedSourceError'
|
||||
}
|
||||
|
||||
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
|
||||
case KnownErrors.NetworkCommunicationError:
|
||||
throw err;
|
||||
case KnownErrors.NonWhitelistedSourceError:
|
||||
return {
|
||||
type: 'error' as const,
|
||||
errorMessage: err.message,
|
||||
state: state,
|
||||
result: null
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: 'exception' as const,
|
||||
|
||||
@@ -3,7 +3,13 @@ import { ContractDefinition } from '../../../../core/ContractDefinition';
|
||||
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
||||
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
||||
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 { NetworkCommunicationError } from "../../../../utils/utils";
|
||||
|
||||
@@ -58,7 +64,7 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
state: currentResult.state,
|
||||
result: null
|
||||
};
|
||||
if (e instanceof ContractError) {
|
||||
if (e instanceof ContractError || e instanceof NonWhitelistedSourceError) {
|
||||
return {
|
||||
...result,
|
||||
error: e.error,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ExecutionContext } from '../core/ExecutionContext';
|
||||
import { ExecutionContextModifier } from '../core/ExecutionContextModifier';
|
||||
import { SmartWeaveError, SmartWeaveErrorType } from '../legacy/errors';
|
||||
import { HandlerApi } from '../core/modules/impl/HandlerExecutorFactory';
|
||||
import { KnownErrors } from '../core/modules/impl/handler/JsHandlerApi';
|
||||
|
||||
function isEvolveCompatible(state: unknown): state is EvolveState {
|
||||
if (!state) {
|
||||
@@ -58,7 +59,10 @@ export class Evolve implements ExecutionContextModifier {
|
||||
|
||||
return executionContext;
|
||||
} catch (e) {
|
||||
if (e.name === 'ContractError' && e.subtype === 'unsafeClientSkip') {
|
||||
if (
|
||||
(e.name === KnownErrors.ContractError && e.subtype === 'unsafeClientSkip') ||
|
||||
e.name == KnownErrors.NonWhitelistedSourceError
|
||||
) {
|
||||
throw e;
|
||||
} else {
|
||||
throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {
|
||||
|
||||
Reference in New Issue
Block a user