feat: uncommitted state for internal writes
This commit is contained in:
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -4,8 +4,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"@idena/vrf-js": "^1.0.1",
|
"@idena/vrf-js": "^1.0.1",
|
||||||
"archiver": "^5.3.0",
|
"archiver": "^5.3.0",
|
||||||
"arweave": "1.11.8",
|
"arweave": "1.11.8",
|
||||||
|
"async-mutex": "^0.4.0",
|
||||||
"elliptic": "^6.5.4",
|
"elliptic": "^6.5.4",
|
||||||
"events": "3.3.0",
|
"events": "3.3.0",
|
||||||
"fast-copy": "^3.0.0",
|
"fast-copy": "^3.0.0",
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ export function handle(state, action) {
|
|||||||
const recipient = _input.recipient;
|
const recipient = _input.recipient;
|
||||||
const amount = _input.amount;
|
const amount = _input.amount;
|
||||||
|
|
||||||
|
if (amount == 0 ) {
|
||||||
|
throw new ContractError('TransferFromZero');
|
||||||
|
}
|
||||||
|
|
||||||
const currentAllowance = _allowances[sender][_msgSender];
|
const currentAllowance = _allowances[sender][_msgSender];
|
||||||
|
|
||||||
if (currentAllowance === undefined || currentAllowance < amount) {
|
if (currentAllowance === undefined || currentAllowance < amount) {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
(() => {
|
||||||
|
// src/thetAR/actions/read/userOrder.ts
|
||||||
|
var create = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
|
||||||
|
const tokenState = await SmartWeave.contracts.readContractState(state.token);
|
||||||
|
//let orderQuantity = param.price;
|
||||||
|
let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id];
|
||||||
|
logger.error(" CREATE Taking tokens: " + orderQuantity);
|
||||||
|
await SmartWeave.contracts.write(state.token, { function: "transferFrom", sender: action.caller, recipient: SmartWeave.contract.id, amount: orderQuantity });
|
||||||
|
state.orders.push(orderQuantity);
|
||||||
|
|
||||||
|
//await SmartWeave.contracts.readContractState(state.token);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
var cancel = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
|
||||||
|
let orderQuantity = state.orders[param.orderId];
|
||||||
|
logger.error("CANCEL Returning tokens: " + orderQuantity);
|
||||||
|
await SmartWeave.contracts.write(state.token, { function: "transfer", to: action.caller, amount: orderQuantity });
|
||||||
|
|
||||||
|
state.orders.splice(param.orderId, 1);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/contract.ts
|
||||||
|
async function handle(state, action) {
|
||||||
|
const func = action.input.function;
|
||||||
|
switch (func) {
|
||||||
|
case "create":
|
||||||
|
return await create(state, action);
|
||||||
|
case "cancel":
|
||||||
|
return await cancel(state, action);
|
||||||
|
default:
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${func}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
403
src/__tests__/integration/data/thethar/thethar-contract-wrc.js
Normal file
403
src/__tests__/integration/data/thethar/thethar-contract-wrc.js
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
(() => {
|
||||||
|
// src/thetAR/actions/common.ts
|
||||||
|
var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr);
|
||||||
|
var hashCheck = async (validHashs, contractTxId) => {
|
||||||
|
const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId);
|
||||||
|
let SrcTxId;
|
||||||
|
tx.get("tags").forEach((tag) => {
|
||||||
|
let key = tag.get("name", { decode: true, string: true });
|
||||||
|
if (key === "Contract-Src") {
|
||||||
|
SrcTxId = tag.get("value", { decode: true, string: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!SrcTxId || !isAddress(SrcTxId)) {
|
||||||
|
throw new ContractError("Cannot find valid srcTxId in contract Tx content!");
|
||||||
|
}
|
||||||
|
const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true });
|
||||||
|
if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
var calcHash = (string) => {
|
||||||
|
var hash = 0, i, chr;
|
||||||
|
if (string.length === 0)
|
||||||
|
return hash;
|
||||||
|
for (i = 0; i < string.length; i++) {
|
||||||
|
chr = string.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + chr;
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
var selectWeightedTokenHolder = async (balances) => {
|
||||||
|
let totalTokens = 0;
|
||||||
|
for (const address of Object.keys(balances)) {
|
||||||
|
totalTokens += balances[address];
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
const r = await getRandomIntNumber(totalTokens);
|
||||||
|
for (const address of Object.keys(balances)) {
|
||||||
|
sum += balances[address];
|
||||||
|
if (r <= sum && balances[address] > 0) {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
};
|
||||||
|
async function getRandomIntNumber(max, uniqueValue = "") {
|
||||||
|
const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue);
|
||||||
|
const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData);
|
||||||
|
const randomBigInt = bigIntFromBytes(hashBytes);
|
||||||
|
return Number(randomBigInt % BigInt(max));
|
||||||
|
}
|
||||||
|
function bigIntFromBytes(byteArr) {
|
||||||
|
let hexString = "";
|
||||||
|
for (const byte of byteArr) {
|
||||||
|
hexString += byte.toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
return BigInt("0x" + hexString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/addPair.ts
|
||||||
|
var addPair = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const tokenAddress = param.tokenAddress;
|
||||||
|
const logoTx = param.logo;
|
||||||
|
const description = param.description;
|
||||||
|
if (!isAddress(tokenAddress)) {
|
||||||
|
throw new ContractError("Token address format error!");
|
||||||
|
}
|
||||||
|
if (!isAddress(logoTx)) {
|
||||||
|
throw new ContractError("You should enter transaction id for Arweave of your logo!");
|
||||||
|
}
|
||||||
|
if (!validDescription(description)) {
|
||||||
|
throw new ContractError("Description you enter is not valid!");
|
||||||
|
}
|
||||||
|
if (action.caller !== state.owner) {
|
||||||
|
const txQty = SmartWeave.transaction.quantity;
|
||||||
|
const txTarget = SmartWeave.transaction.target;
|
||||||
|
if (txTarget !== state.owner) {
|
||||||
|
throw new ContractError("AddPair fee sent to wrong target!");
|
||||||
|
}
|
||||||
|
if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) {
|
||||||
|
throw new ContractError("AddPair fee not right!");
|
||||||
|
}
|
||||||
|
if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) {
|
||||||
|
throw new ContractError("Pst contract validation check failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) {
|
||||||
|
throw new ContractError("Pair already exists!");
|
||||||
|
}
|
||||||
|
const tokenState = await SmartWeave.contracts.readContractState(tokenAddress);
|
||||||
|
state.maxPairId++;
|
||||||
|
state.pairInfos.push({
|
||||||
|
pairId: state.maxPairId,
|
||||||
|
tokenAddress,
|
||||||
|
logo: logoTx,
|
||||||
|
description,
|
||||||
|
name: tokenState.name,
|
||||||
|
symbol: tokenState.symbol,
|
||||||
|
decimals: tokenState.decimals
|
||||||
|
});
|
||||||
|
state.orderInfos[state.maxPairId] = {
|
||||||
|
currentPrice: void 0,
|
||||||
|
orders: []
|
||||||
|
};
|
||||||
|
for (const user in state.userOrders) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) {
|
||||||
|
let userOrder2 = state.userOrders[user];
|
||||||
|
userOrder2[state.maxPairId] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc);
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/createOrder.ts
|
||||||
|
var createOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) {
|
||||||
|
throw new ContractError("PairId not valid!");
|
||||||
|
}
|
||||||
|
if (param.price !== void 0 && param.price !== null) {
|
||||||
|
if (typeof param.price !== "number") {
|
||||||
|
throw new ContractError("Price must be a number!");
|
||||||
|
}
|
||||||
|
if (param.price <= 0 || !Number.isInteger(param.price)) {
|
||||||
|
throw new ContractError("Price must be positive integer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newOrder = {
|
||||||
|
creator: action.caller,
|
||||||
|
orderId: SmartWeave.transaction.id,
|
||||||
|
direction: param.direction,
|
||||||
|
quantity: await checkOrderQuantity(state, action),
|
||||||
|
price: param.price
|
||||||
|
};
|
||||||
|
let selectedFeeRecvr = void 0;
|
||||||
|
try {
|
||||||
|
selectedFeeRecvr = await selectWeightedTokenHolder(await tokenBalances(state.thetarTokenAddress));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr);
|
||||||
|
state.orderInfos[param.pairId].orders = newOrderbook;
|
||||||
|
state.userOrders = newUserOrders;
|
||||||
|
if (!isNaN(currentPrice) && isFinite(currentPrice)) {
|
||||||
|
state.orderInfos[param.pairId].currentPrice = currentPrice;
|
||||||
|
}
|
||||||
|
for await (const tx of transactions) {
|
||||||
|
const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId);
|
||||||
|
const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress;
|
||||||
|
await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity });
|
||||||
|
}
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
var tokenBalances = async (tokenAddress) => {
|
||||||
|
return (await SmartWeave.contracts.readContractState(tokenAddress)).balances;
|
||||||
|
};
|
||||||
|
var checkOrderQuantity = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId);
|
||||||
|
const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress;
|
||||||
|
const tokenState = await SmartWeave.contracts.readContractState(tokenAddress);
|
||||||
|
let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id];
|
||||||
|
await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity });
|
||||||
|
if (param.direction === "buy" && param.price) {
|
||||||
|
orderQuantity = Math.floor(orderQuantity / param.price);
|
||||||
|
}
|
||||||
|
return orderQuantity;
|
||||||
|
};
|
||||||
|
var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => {
|
||||||
|
let transactions = Array();
|
||||||
|
const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy";
|
||||||
|
let totalTradePrice = 0;
|
||||||
|
let totalTradeVolume = 0;
|
||||||
|
const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => {
|
||||||
|
if (newOrder.direction === "buy") {
|
||||||
|
return a.price > b.price ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return a.price > b.price ? -1 : 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const orderType = newOrder.price ? "limit" : "market";
|
||||||
|
if (reverseOrderbook.length === 0 && orderType === "market") {
|
||||||
|
throw new ContractError(`The first order must be limit type!`);
|
||||||
|
}
|
||||||
|
const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade";
|
||||||
|
for (let i = 0; i < reverseOrderbook.length; i++) {
|
||||||
|
const order = reverseOrderbook[i];
|
||||||
|
if (orderType === "limit" && order.price !== newOrder.price) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetPrice = order.price;
|
||||||
|
const orderAmount = order.quantity;
|
||||||
|
const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice);
|
||||||
|
const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt;
|
||||||
|
totalTradePrice += targetPrice * targetAmout;
|
||||||
|
totalTradeVolume += targetAmout;
|
||||||
|
if (targetAmout === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio);
|
||||||
|
const tradeFee = Math.floor(targetAmout * feeRatio);
|
||||||
|
const dominentSwap = targetAmout * targetPrice - dominentFee;
|
||||||
|
const tradeSwap = targetAmout - tradeFee;
|
||||||
|
const buyer = newOrder.direction === "buy" ? newOrder : order;
|
||||||
|
const seller = newOrder.direction === "buy" ? order : newOrder;
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "dominent",
|
||||||
|
to: seller.creator,
|
||||||
|
quantity: dominentSwap
|
||||||
|
});
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "trade",
|
||||||
|
to: buyer.creator,
|
||||||
|
quantity: tradeSwap
|
||||||
|
});
|
||||||
|
if (selectedFeeRecvr) {
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "dominent",
|
||||||
|
to: selectedFeeRecvr,
|
||||||
|
quantity: dominentFee
|
||||||
|
});
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "trade",
|
||||||
|
to: selectedFeeRecvr,
|
||||||
|
quantity: tradeFee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
order.quantity -= targetAmout;
|
||||||
|
if (order.quantity === 0) {
|
||||||
|
orderbook = orderbook.filter((v) => v.orderId !== order.orderId);
|
||||||
|
}
|
||||||
|
let userOrderInfos = userOrders[order.creator][newOrderPairId];
|
||||||
|
let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId);
|
||||||
|
userOrderInfos[matchedOrderIdx].quantity -= targetAmout;
|
||||||
|
if (userOrderInfos[matchedOrderIdx].quantity === 0) {
|
||||||
|
userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId);
|
||||||
|
}
|
||||||
|
newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice;
|
||||||
|
}
|
||||||
|
if (orderType === "market" && newOrder.quantity !== 0) {
|
||||||
|
transactions.push({
|
||||||
|
tokenType: newOrderTokenType,
|
||||||
|
to: newOrder.creator,
|
||||||
|
quantity: newOrder.quantity
|
||||||
|
});
|
||||||
|
newOrder.quantity = 0;
|
||||||
|
}
|
||||||
|
if (orderType === "limit" && newOrder.quantity !== 0) {
|
||||||
|
orderbook.push({ ...newOrder });
|
||||||
|
}
|
||||||
|
if (newOrder.quantity !== 0) {
|
||||||
|
if (userOrders[caller] === void 0) {
|
||||||
|
userOrders[caller] = {};
|
||||||
|
}
|
||||||
|
if (userOrders[caller][newOrderPairId] === void 0) {
|
||||||
|
userOrders[caller][newOrderPairId] = [];
|
||||||
|
}
|
||||||
|
userOrders[caller][newOrderPairId].push({ ...newOrder });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
newOrderbook: orderbook,
|
||||||
|
newUserOrders: userOrders,
|
||||||
|
transactions,
|
||||||
|
currentPrice: totalTradePrice / totalTradeVolume
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/deposit.ts
|
||||||
|
var deposit = async (state, action) => {
|
||||||
|
logger.error("Token: " + action.input.params.token);
|
||||||
|
logger.error("Amount: " + action.input.params.amount);
|
||||||
|
await SmartWeave.contracts.write(action.input.params.token, {
|
||||||
|
function: "transferFrom",
|
||||||
|
from: action.caller,
|
||||||
|
to: SmartWeave.contract.id,
|
||||||
|
amount: action.input.params.amount
|
||||||
|
});
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/cancelOrder.ts
|
||||||
|
var cancelOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const orderId = param.orderId;
|
||||||
|
const pairId = param.pairId;
|
||||||
|
if (!isAddress(orderId)) {
|
||||||
|
throw new ContractError(`OrderId not found: ${param.orderId}!`);
|
||||||
|
}
|
||||||
|
if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) {
|
||||||
|
throw new ContractError("PairId not valid!");
|
||||||
|
}
|
||||||
|
const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId);
|
||||||
|
const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId);
|
||||||
|
if (!orderInfo2) {
|
||||||
|
throw new ContractError(`Cannot get access to pairId: ${pairId}!`);
|
||||||
|
}
|
||||||
|
if (!pairInfo2) {
|
||||||
|
throw new ContractError(`Pair info record not found: ${pairId}!`);
|
||||||
|
}
|
||||||
|
const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress;
|
||||||
|
const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity;
|
||||||
|
await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity });
|
||||||
|
let ordersForUser = state.userOrders[action.caller][pairId];
|
||||||
|
state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId);
|
||||||
|
let ordersForPair = state.orderInfos[pairId].orders;
|
||||||
|
state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/addTokenHash.ts
|
||||||
|
var addTokenHash = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const hash = param.hash;
|
||||||
|
if (action.caller !== state.owner) {
|
||||||
|
throw new ContractError("You have no permission to modify hash list!");
|
||||||
|
}
|
||||||
|
state.tokenSrcTemplateHashs.push(hash);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/pairInfo.ts
|
||||||
|
var pairInfo = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairId = param.pairId;
|
||||||
|
let result;
|
||||||
|
if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) {
|
||||||
|
throw new ContractError(`Invalid pairId!`);
|
||||||
|
}
|
||||||
|
result = state.pairInfos.filter((i) => i.pairId === pairId)[0];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/pairInfos.ts
|
||||||
|
var pairInfos = async (state, action) => {
|
||||||
|
let result;
|
||||||
|
result = state.pairInfos;
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/orderInfos.ts
|
||||||
|
var orderInfos = async (state, action) => {
|
||||||
|
let result;
|
||||||
|
result = state.orderInfos;
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/orderInfo.ts
|
||||||
|
var orderInfo = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairId = param.pairId;
|
||||||
|
let result;
|
||||||
|
if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) {
|
||||||
|
throw new ContractError(`Invalid pairId!`);
|
||||||
|
}
|
||||||
|
result = state.orderInfos[pairId];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/userOrder.ts
|
||||||
|
var userOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let address = param.address;
|
||||||
|
let result;
|
||||||
|
if (!isAddress(address)) {
|
||||||
|
throw new ContractError(`Invalid wallet address!`);
|
||||||
|
}
|
||||||
|
result = state.userOrders[address];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/contract.ts
|
||||||
|
async function handle(state, action) {
|
||||||
|
const func = action.input.function;
|
||||||
|
switch (func) {
|
||||||
|
case "addPair":
|
||||||
|
return await addPair(state, action);
|
||||||
|
case "createOrder":
|
||||||
|
return await createOrder(state, action);
|
||||||
|
case "cancelOrder":
|
||||||
|
return await cancelOrder(state, action);
|
||||||
|
case "pairInfo":
|
||||||
|
return await pairInfo(state, action);
|
||||||
|
case "pairInfos":
|
||||||
|
return await pairInfos(state, action);
|
||||||
|
case "orderInfo":
|
||||||
|
return await orderInfo(state, action);
|
||||||
|
case "orderInfos":
|
||||||
|
return await orderInfos(state, action);
|
||||||
|
case "addTokenHash":
|
||||||
|
return await addTokenHash(state, action);
|
||||||
|
case "userOrder":
|
||||||
|
return await userOrder(state, action);
|
||||||
|
case "deposit":
|
||||||
|
return await deposit(state, action);
|
||||||
|
default:
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${func}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
411
src/__tests__/integration/data/thethar/thethar-contract.js
Normal file
411
src/__tests__/integration/data/thethar/thethar-contract.js
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
(() => {
|
||||||
|
// src/thetAR/actions/common.ts
|
||||||
|
var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr);
|
||||||
|
var hashCheck = async (validHashs, contractTxId) => {
|
||||||
|
const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId);
|
||||||
|
let SrcTxId;
|
||||||
|
tx.get("tags").forEach((tag) => {
|
||||||
|
let key = tag.get("name", { decode: true, string: true });
|
||||||
|
if (key === "Contract-Src") {
|
||||||
|
SrcTxId = tag.get("value", { decode: true, string: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!SrcTxId || !isAddress(SrcTxId)) {
|
||||||
|
throw new ContractError("Cannot find valid srcTxId in contract Tx content!");
|
||||||
|
}
|
||||||
|
const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true });
|
||||||
|
if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
var calcHash = (string) => {
|
||||||
|
var hash = 0, i, chr;
|
||||||
|
if (string.length === 0)
|
||||||
|
return hash;
|
||||||
|
for (i = 0; i < string.length; i++) {
|
||||||
|
chr = string.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + chr;
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
var selectWeightedTokenHolder = async (balances) => {
|
||||||
|
let totalTokens = 0;
|
||||||
|
for (const address of Object.keys(balances)) {
|
||||||
|
totalTokens += balances[address];
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
const r = await getRandomIntNumber(totalTokens);
|
||||||
|
for (const address of Object.keys(balances)) {
|
||||||
|
sum += balances[address];
|
||||||
|
if (r <= sum && balances[address] > 0) {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
};
|
||||||
|
async function getRandomIntNumber(max, uniqueValue = "") {
|
||||||
|
const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue);
|
||||||
|
const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData);
|
||||||
|
const randomBigInt = bigIntFromBytes(hashBytes);
|
||||||
|
return Number(randomBigInt % BigInt(max));
|
||||||
|
}
|
||||||
|
function bigIntFromBytes(byteArr) {
|
||||||
|
let hexString = "";
|
||||||
|
for (const byte of byteArr) {
|
||||||
|
hexString += byte.toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
return BigInt("0x" + hexString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/addPair.ts
|
||||||
|
var addPair = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const tokenAddress = param.tokenAddress;
|
||||||
|
const logoTx = param.logo;
|
||||||
|
const description = param.description;
|
||||||
|
if (!isAddress(tokenAddress)) {
|
||||||
|
throw new ContractError("Token address format error!");
|
||||||
|
}
|
||||||
|
if (!isAddress(logoTx)) {
|
||||||
|
throw new ContractError("You should enter transaction id for Arweave of your logo!");
|
||||||
|
}
|
||||||
|
if (!validDescription(description)) {
|
||||||
|
throw new ContractError("Description you enter is not valid!");
|
||||||
|
}
|
||||||
|
if (action.caller !== state.owner) {
|
||||||
|
const txQty = SmartWeave.transaction.quantity;
|
||||||
|
const txTarget = SmartWeave.transaction.target;
|
||||||
|
if (txTarget !== state.owner) {
|
||||||
|
throw new ContractError("AddPair fee sent to wrong target!");
|
||||||
|
}
|
||||||
|
if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) {
|
||||||
|
throw new ContractError("AddPair fee not right!");
|
||||||
|
}
|
||||||
|
if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) {
|
||||||
|
throw new ContractError("Pst contract validation check failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) {
|
||||||
|
throw new ContractError("Pair already exists!");
|
||||||
|
}
|
||||||
|
const tokenState = await SmartWeave.contracts.readContractState(tokenAddress);
|
||||||
|
state.maxPairId++;
|
||||||
|
state.pairInfos.push({
|
||||||
|
pairId: state.maxPairId,
|
||||||
|
tokenAddress,
|
||||||
|
logo: logoTx,
|
||||||
|
description,
|
||||||
|
name: tokenState.name,
|
||||||
|
symbol: tokenState.symbol,
|
||||||
|
decimals: tokenState.decimals
|
||||||
|
});
|
||||||
|
state.orderInfos[state.maxPairId] = {
|
||||||
|
currentPrice: void 0,
|
||||||
|
orders: []
|
||||||
|
};
|
||||||
|
for (const user in state.userOrders) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) {
|
||||||
|
let userOrder2 = state.userOrders[user];
|
||||||
|
userOrder2[state.maxPairId] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc);
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/createOrder.ts
|
||||||
|
var createOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) {
|
||||||
|
throw new ContractError("PairId not valid!");
|
||||||
|
}
|
||||||
|
if (param.price !== void 0 && param.price !== null) {
|
||||||
|
if (typeof param.price !== "number") {
|
||||||
|
throw new ContractError("Price must be a number!");
|
||||||
|
}
|
||||||
|
if (param.price <= 0 || !Number.isInteger(param.price)) {
|
||||||
|
throw new ContractError("Price must be positive integer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let {orderQuantity, updatedState} = await checkOrderQuantity(state, action);
|
||||||
|
const newOrder = {
|
||||||
|
creator: action.caller,
|
||||||
|
orderId: SmartWeave.transaction.id,
|
||||||
|
direction: param.direction,
|
||||||
|
quantity: orderQuantity,
|
||||||
|
price: param.price
|
||||||
|
};
|
||||||
|
let selectedFeeRecvr = void 0;
|
||||||
|
try {
|
||||||
|
selectedFeeRecvr = await selectWeightedTokenHolder(tokenBalances(updatedState));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr);
|
||||||
|
state.orderInfos[param.pairId].orders = newOrderbook;
|
||||||
|
state.userOrders = newUserOrders;
|
||||||
|
if (!isNaN(currentPrice) && isFinite(currentPrice)) {
|
||||||
|
state.orderInfos[param.pairId].currentPrice = currentPrice;
|
||||||
|
}
|
||||||
|
for await (const tx of transactions) {
|
||||||
|
const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId);
|
||||||
|
const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress;
|
||||||
|
await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity });
|
||||||
|
}
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
var tokenBalances = (updatedState) => {
|
||||||
|
return updatedState.balances;
|
||||||
|
};
|
||||||
|
var checkOrderQuantity = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId);
|
||||||
|
const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress;
|
||||||
|
const tokenState = await SmartWeave.contracts.readContractState(tokenAddress);
|
||||||
|
//let orderQuantity = param.price;
|
||||||
|
let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id];
|
||||||
|
//WASM version
|
||||||
|
//await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity });
|
||||||
|
//JS version
|
||||||
|
logger.error("CREATE Taking tokens: " + orderQuantity);
|
||||||
|
updatedState = (await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", sender: action.caller, recipient: SmartWeave.contract.id, amount: orderQuantity })).state;
|
||||||
|
logger.error("STATE:", updatedState);
|
||||||
|
if (param.direction === "buy" && param.price) {
|
||||||
|
orderQuantity = Math.floor(orderQuantity / param.price);
|
||||||
|
}
|
||||||
|
return {orderQuantity, updatedState};
|
||||||
|
};
|
||||||
|
var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => {
|
||||||
|
let transactions = Array();
|
||||||
|
const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy";
|
||||||
|
let totalTradePrice = 0;
|
||||||
|
let totalTradeVolume = 0;
|
||||||
|
const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => {
|
||||||
|
if (newOrder.direction === "buy") {
|
||||||
|
return a.price > b.price ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return a.price > b.price ? -1 : 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const orderType = newOrder.price ? "limit" : "market";
|
||||||
|
if (reverseOrderbook.length === 0 && orderType === "market") {
|
||||||
|
throw new ContractError(`The first order must be limit type!`);
|
||||||
|
}
|
||||||
|
const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade";
|
||||||
|
for (let i = 0; i < reverseOrderbook.length; i++) {
|
||||||
|
const order = reverseOrderbook[i];
|
||||||
|
if (orderType === "limit" && order.price !== newOrder.price) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetPrice = order.price;
|
||||||
|
const orderAmount = order.quantity;
|
||||||
|
const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice);
|
||||||
|
const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt;
|
||||||
|
totalTradePrice += targetPrice * targetAmout;
|
||||||
|
totalTradeVolume += targetAmout;
|
||||||
|
if (targetAmout === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio);
|
||||||
|
const tradeFee = Math.floor(targetAmout * feeRatio);
|
||||||
|
const dominentSwap = targetAmout * targetPrice - dominentFee;
|
||||||
|
const tradeSwap = targetAmout - tradeFee;
|
||||||
|
const buyer = newOrder.direction === "buy" ? newOrder : order;
|
||||||
|
const seller = newOrder.direction === "buy" ? order : newOrder;
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "dominent",
|
||||||
|
to: seller.creator,
|
||||||
|
quantity: dominentSwap
|
||||||
|
});
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "trade",
|
||||||
|
to: buyer.creator,
|
||||||
|
quantity: tradeSwap
|
||||||
|
});
|
||||||
|
if (selectedFeeRecvr) {
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "dominent",
|
||||||
|
to: selectedFeeRecvr,
|
||||||
|
quantity: dominentFee
|
||||||
|
});
|
||||||
|
transactions.push({
|
||||||
|
tokenType: "trade",
|
||||||
|
to: selectedFeeRecvr,
|
||||||
|
quantity: tradeFee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
order.quantity -= targetAmout;
|
||||||
|
if (order.quantity === 0) {
|
||||||
|
orderbook = orderbook.filter((v) => v.orderId !== order.orderId);
|
||||||
|
}
|
||||||
|
let userOrderInfos = userOrders[order.creator][newOrderPairId];
|
||||||
|
let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId);
|
||||||
|
userOrderInfos[matchedOrderIdx].quantity -= targetAmout;
|
||||||
|
if (userOrderInfos[matchedOrderIdx].quantity === 0) {
|
||||||
|
userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId);
|
||||||
|
}
|
||||||
|
newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice;
|
||||||
|
}
|
||||||
|
if (orderType === "market" && newOrder.quantity !== 0) {
|
||||||
|
transactions.push({
|
||||||
|
tokenType: newOrderTokenType,
|
||||||
|
to: newOrder.creator,
|
||||||
|
quantity: newOrder.quantity
|
||||||
|
});
|
||||||
|
newOrder.quantity = 0;
|
||||||
|
}
|
||||||
|
if (orderType === "limit" && newOrder.quantity !== 0) {
|
||||||
|
orderbook.push({ ...newOrder });
|
||||||
|
}
|
||||||
|
if (newOrder.quantity !== 0) {
|
||||||
|
if (userOrders[caller] === void 0) {
|
||||||
|
userOrders[caller] = {};
|
||||||
|
}
|
||||||
|
if (userOrders[caller][newOrderPairId] === void 0) {
|
||||||
|
userOrders[caller][newOrderPairId] = [];
|
||||||
|
}
|
||||||
|
userOrders[caller][newOrderPairId].push({ ...newOrder });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
newOrderbook: orderbook,
|
||||||
|
newUserOrders: userOrders,
|
||||||
|
transactions,
|
||||||
|
currentPrice: totalTradePrice / totalTradeVolume
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/deposit.ts
|
||||||
|
var deposit = async (state, action) => {
|
||||||
|
logger.error("Token: " + action.input.params.token);
|
||||||
|
logger.error("Amount: " + action.input.params.amount);
|
||||||
|
await SmartWeave.contracts.write(action.input.params.token, {
|
||||||
|
function: "transferFrom",
|
||||||
|
from: action.caller,
|
||||||
|
to: SmartWeave.contract.id,
|
||||||
|
amount: action.input.params.amount
|
||||||
|
});
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/cancelOrder.ts
|
||||||
|
var cancelOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const orderId = param.orderId;
|
||||||
|
const pairId = param.pairId;
|
||||||
|
if (!isAddress(orderId)) {
|
||||||
|
throw new ContractError(`OrderId not found: ${param.orderId}!`);
|
||||||
|
}
|
||||||
|
if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) {
|
||||||
|
throw new ContractError("PairId not valid!");
|
||||||
|
}
|
||||||
|
const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId);
|
||||||
|
const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId);
|
||||||
|
if (!orderInfo2) {
|
||||||
|
throw new ContractError(`Cannot get access to pairId: ${pairId}!`);
|
||||||
|
}
|
||||||
|
if (!pairInfo2) {
|
||||||
|
throw new ContractError(`Pair info record not found: ${pairId}!`);
|
||||||
|
}
|
||||||
|
const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress;
|
||||||
|
const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity;
|
||||||
|
logger.error("CANCEL Returning tokens: " + quantity);
|
||||||
|
await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity });
|
||||||
|
let ordersForUser = state.userOrders[action.caller][pairId];
|
||||||
|
state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId);
|
||||||
|
let ordersForPair = state.orderInfos[pairId].orders;
|
||||||
|
state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/write/addTokenHash.ts
|
||||||
|
var addTokenHash = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
const hash = param.hash;
|
||||||
|
if (action.caller !== state.owner) {
|
||||||
|
throw new ContractError("You have no permission to modify hash list!");
|
||||||
|
}
|
||||||
|
state.tokenSrcTemplateHashs.push(hash);
|
||||||
|
return { state };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/pairInfo.ts
|
||||||
|
var pairInfo = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairId = param.pairId;
|
||||||
|
let result;
|
||||||
|
if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) {
|
||||||
|
throw new ContractError(`Invalid pairId!`);
|
||||||
|
}
|
||||||
|
result = state.pairInfos.filter((i) => i.pairId === pairId)[0];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/pairInfos.ts
|
||||||
|
var pairInfos = async (state, action) => {
|
||||||
|
let result;
|
||||||
|
result = state.pairInfos;
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/orderInfos.ts
|
||||||
|
var orderInfos = async (state, action) => {
|
||||||
|
let result;
|
||||||
|
result = state.orderInfos;
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/orderInfo.ts
|
||||||
|
var orderInfo = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let pairId = param.pairId;
|
||||||
|
let result;
|
||||||
|
if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) {
|
||||||
|
throw new ContractError(`Invalid pairId!`);
|
||||||
|
}
|
||||||
|
result = state.orderInfos[pairId];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/actions/read/userOrder.ts
|
||||||
|
var userOrder = async (state, action) => {
|
||||||
|
const param = action.input.params;
|
||||||
|
let address = param.address;
|
||||||
|
let result;
|
||||||
|
if (!isAddress(address)) {
|
||||||
|
throw new ContractError(`Invalid wallet address!`);
|
||||||
|
}
|
||||||
|
result = state.userOrders[address];
|
||||||
|
return { result };
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/thetAR/contract.ts
|
||||||
|
async function handle(state, action) {
|
||||||
|
const func = action.input.function;
|
||||||
|
switch (func) {
|
||||||
|
case "addPair":
|
||||||
|
return await addPair(state, action);
|
||||||
|
case "createOrder":
|
||||||
|
return await createOrder(state, action);
|
||||||
|
case "cancelOrder":
|
||||||
|
return await cancelOrder(state, action);
|
||||||
|
case "pairInfo":
|
||||||
|
return await pairInfo(state, action);
|
||||||
|
case "pairInfos":
|
||||||
|
return await pairInfos(state, action);
|
||||||
|
case "orderInfo":
|
||||||
|
return await orderInfo(state, action);
|
||||||
|
case "orderInfos":
|
||||||
|
return await orderInfos(state, action);
|
||||||
|
case "addTokenHash":
|
||||||
|
return await addTokenHash(state, action);
|
||||||
|
case "userOrder":
|
||||||
|
return await userOrder(state, action);
|
||||||
|
case "deposit":
|
||||||
|
return await deposit(state, action);
|
||||||
|
default:
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${func}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
56
src/__tests__/integration/data/wrc-20/src/action.rs
Normal file
56
src/__tests__/integration/data/wrc-20/src/action.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult;
|
||||||
|
use crate::error::ContractError;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "function")]
|
||||||
|
pub enum Action {
|
||||||
|
Transfer {
|
||||||
|
to: String,
|
||||||
|
amount: u64,
|
||||||
|
},
|
||||||
|
TransferFrom {
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
amount: u64
|
||||||
|
},
|
||||||
|
BalanceOf {
|
||||||
|
target: String
|
||||||
|
},
|
||||||
|
TotalSupply {
|
||||||
|
},
|
||||||
|
Approve {
|
||||||
|
spender: String,
|
||||||
|
amount: u64,
|
||||||
|
},
|
||||||
|
Allowance {
|
||||||
|
owner: String,
|
||||||
|
spender: String
|
||||||
|
},
|
||||||
|
Evolve {
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", untagged)]
|
||||||
|
pub enum QueryResponseMsg {
|
||||||
|
Balance {
|
||||||
|
ticker: String,
|
||||||
|
target: String,
|
||||||
|
balance: u64,
|
||||||
|
},
|
||||||
|
Allowance {
|
||||||
|
ticker: String,
|
||||||
|
owner: String,
|
||||||
|
spender: String,
|
||||||
|
allowance: u64,
|
||||||
|
},
|
||||||
|
TotalSupply {
|
||||||
|
value: u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ActionResult = Result<HandlerResult<State, QueryResponseMsg>, ContractError>;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use crate::state::State;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult;
|
||||||
|
use crate::action::{QueryResponseMsg::Allowance, ActionResult};
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse;
|
||||||
|
use warp_wasm_utils::contract_utils::js_imports::{Transaction};
|
||||||
|
|
||||||
|
pub fn allowance(state: State, owner: String, spender: String) -> ActionResult {
|
||||||
|
Ok(QueryResponse(
|
||||||
|
Allowance {
|
||||||
|
ticker: state.symbol,
|
||||||
|
allowance: __get_allowance(&state.allowances, &owner, &spender),
|
||||||
|
owner,
|
||||||
|
spender
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn approve(mut state: State, spender: String, amount: u64) -> ActionResult {
|
||||||
|
let caller = Transaction::owner();
|
||||||
|
__set_allowance(&mut state.allowances, caller, spender, amount);
|
||||||
|
Ok(HandlerResult::NewState(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480
|
||||||
|
// Not a part of the contract API - used internally within the crate.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn __set_allowance(allowances: &mut HashMap<String, HashMap<String, u64>>, owner: String, spender: String, amount: u64) {
|
||||||
|
if amount > 0 {
|
||||||
|
*allowances
|
||||||
|
.entry(owner)
|
||||||
|
.or_default()
|
||||||
|
.entry(spender)
|
||||||
|
.or_default() = amount;
|
||||||
|
} else { //Prune state
|
||||||
|
match allowances.get_mut(&owner) {
|
||||||
|
Some(spender_allowances) => {
|
||||||
|
spender_allowances.remove(&spender);
|
||||||
|
if spender_allowances.is_empty() {
|
||||||
|
allowances.remove(&owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480
|
||||||
|
// Not a part of the contract API - used internally within the crate.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn __get_allowance(allowances: &HashMap<String, HashMap<String, u64>>, owner: &String, spender: &String) -> u64 {
|
||||||
|
return *allowances
|
||||||
|
.get(owner)
|
||||||
|
.map_or(&0, |spenders| {
|
||||||
|
spenders.get(spender).unwrap_or(&0)
|
||||||
|
});
|
||||||
|
}
|
||||||
23
src/__tests__/integration/data/wrc-20/src/actions/balance.rs
Normal file
23
src/__tests__/integration/data/wrc-20/src/actions/balance.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use crate::state::State;
|
||||||
|
use crate::action::{QueryResponseMsg::Balance,QueryResponseMsg::TotalSupply, ActionResult};
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse;
|
||||||
|
|
||||||
|
pub fn balance_of(state: State, target: String) -> ActionResult {
|
||||||
|
Ok(QueryResponse(
|
||||||
|
Balance {
|
||||||
|
balance: *state.balances.get( & target).unwrap_or(&0),
|
||||||
|
ticker: state.symbol,
|
||||||
|
target
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_supply(state: State) -> ActionResult {
|
||||||
|
Ok(QueryResponse(
|
||||||
|
TotalSupply {
|
||||||
|
value: state.total_supply
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
17
src/__tests__/integration/data/wrc-20/src/actions/evolve.rs
Normal file
17
src/__tests__/integration/data/wrc-20/src/actions/evolve.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::error::ContractError::{EvolveNotAllowed, OnlyOwnerCanEvolve};
|
||||||
|
use crate::state::{State};
|
||||||
|
use warp_wasm_utils::contract_utils::js_imports::Transaction;
|
||||||
|
use crate::action::ActionResult;
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult;
|
||||||
|
|
||||||
|
pub fn evolve(mut state: State, value: String) -> ActionResult {
|
||||||
|
match state.can_evolve {
|
||||||
|
Some(can_evolve) => if can_evolve && state.owner == Transaction::owner() {
|
||||||
|
state.evolve = Option::from(value);
|
||||||
|
Ok(HandlerResult::NewState(state))
|
||||||
|
} else {
|
||||||
|
Err(OnlyOwnerCanEvolve)
|
||||||
|
},
|
||||||
|
None => Err(EvolveNotAllowed),
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/__tests__/integration/data/wrc-20/src/actions/mod.rs
Normal file
5
src/__tests__/integration/data/wrc-20/src/actions/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod evolve;
|
||||||
|
pub mod balance;
|
||||||
|
pub mod transfers;
|
||||||
|
pub mod allowances;
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::error::ContractError::{CallerBalanceNotEnough, CallerAllowanceNotEnough};
|
||||||
|
use crate::actions::allowances::{__set_allowance, __get_allowance};
|
||||||
|
use crate::state::State;
|
||||||
|
use crate::action::ActionResult;
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult;
|
||||||
|
use warp_wasm_utils::contract_utils::js_imports::{SmartWeave};
|
||||||
|
|
||||||
|
pub fn transfer(state: State, to: String, amount: u64) -> ActionResult {
|
||||||
|
let caller = SmartWeave::caller();
|
||||||
|
return _transfer(state, caller, to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transfer_from(mut state: State, from: String, to: String, amount: u64) -> ActionResult {
|
||||||
|
let caller = SmartWeave::caller();
|
||||||
|
|
||||||
|
//Checking allowance
|
||||||
|
let allowance = __get_allowance(&state.allowances, &from, &caller);
|
||||||
|
|
||||||
|
if allowance < amount {
|
||||||
|
return Err(CallerAllowanceNotEnough(allowance));
|
||||||
|
}
|
||||||
|
|
||||||
|
__set_allowance(&mut state.allowances, from.to_owned(), caller, allowance - amount);
|
||||||
|
|
||||||
|
return _transfer(state, from, to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _transfer(mut state: State, from: String, to: String, amount: u64) -> ActionResult {
|
||||||
|
// Checking if caller has enough funds
|
||||||
|
let balances = &mut state.balances;
|
||||||
|
let from_balance = *balances.get(&from).unwrap_or(&0);
|
||||||
|
if from_balance < amount {
|
||||||
|
return Err(CallerBalanceNotEnough(from_balance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update caller balance or prune state if the new value is 0
|
||||||
|
if from_balance - amount == 0 {
|
||||||
|
balances.remove(&from);
|
||||||
|
} else {
|
||||||
|
balances.insert(from, from_balance - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update target balance
|
||||||
|
*balances.entry(to).or_insert(0) += amount;
|
||||||
|
|
||||||
|
Ok(HandlerResult::NewState(state))
|
||||||
|
}
|
||||||
38
src/__tests__/integration/data/wrc-20/src/contract.rs
Normal file
38
src/__tests__/integration/data/wrc-20/src/contract.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::action::{Action, ActionResult};
|
||||||
|
use crate::actions::transfers::transfer;
|
||||||
|
use crate::actions::transfers::transfer_from;
|
||||||
|
use crate::actions::balance::balance_of;
|
||||||
|
use crate::actions::balance::total_supply;
|
||||||
|
use crate::actions::allowances::approve;
|
||||||
|
use crate::actions::allowances::allowance;
|
||||||
|
use crate::actions::evolve::evolve;
|
||||||
|
use warp_wasm_utils::contract_utils::js_imports::{Block, Contract, log, SmartWeave, Transaction};
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
pub async fn handle(current_state: State, action: Action) -> ActionResult {
|
||||||
|
|
||||||
|
//Example of accessing functions imported from js:
|
||||||
|
log("log from contract");
|
||||||
|
log(&("Transaction::id()".to_owned() + &Transaction::id()));
|
||||||
|
log(&("Transaction::owner()".to_owned() + &Transaction::owner()));
|
||||||
|
log(&("Transaction::target()".to_owned() + &Transaction::target()));
|
||||||
|
|
||||||
|
log(&("Block::height()".to_owned() + &Block::height().to_string()));
|
||||||
|
log(&("Block::indep_hash()".to_owned() + &Block::indep_hash()));
|
||||||
|
log(&("Block::timestamp()".to_owned() + &Block::timestamp().to_string()));
|
||||||
|
|
||||||
|
log(&("Contract::id()".to_owned() + &Contract::id()));
|
||||||
|
log(&("Contract::owner()".to_owned() + &Contract::owner()));
|
||||||
|
|
||||||
|
log(&("SmartWeave::caller()".to_owned() + &SmartWeave::caller()));
|
||||||
|
|
||||||
|
match action {
|
||||||
|
Action::Transfer { to, amount } => transfer(current_state, to, amount),
|
||||||
|
Action::TransferFrom { from, to, amount } => transfer_from(current_state, from, to, amount),
|
||||||
|
Action::BalanceOf { target } => balance_of(current_state, target),
|
||||||
|
Action::TotalSupply { } => total_supply(current_state),
|
||||||
|
Action::Approve { spender, amount } => approve(current_state, spender, amount),
|
||||||
|
Action::Allowance { owner, spender } => allowance(current_state, owner, spender),
|
||||||
|
Action::Evolve { value } => evolve(current_state, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# contract_utils module
|
||||||
|
|
||||||
|
This is a module with boilerplate code for each SmartWeave RUST contract.
|
||||||
|
**Please don't modify it unless you 100% know what you are doing!**
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/////////////////////////////////////////////////////
|
||||||
|
/////////////// DO NOT MODIFY THIS FILE /////////////
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use serde_json::Error;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::action::{Action, QueryResponseMsg};
|
||||||
|
use crate::contract;
|
||||||
|
use warp_wasm_utils::contract_utils::handler_result::HandlerResult;
|
||||||
|
use crate::error::ContractError;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Note: in order do optimize communication between host and the WASM module,
|
||||||
|
we're storing the state inside the WASM module (for the time of state evaluation).
|
||||||
|
This allows to reduce the overhead of passing the state back and forth
|
||||||
|
between the host and module with each contract interaction.
|
||||||
|
In case of bigger states this overhead can be huge.
|
||||||
|
Same approach has been implemented for the AssemblyScript version.
|
||||||
|
|
||||||
|
So the flow (from the SDK perspective) is:
|
||||||
|
1. SDK calls exported WASM module function "initState" (with lastly cached state or initial state,
|
||||||
|
if cache is empty) - which initializes the state in the WASM module.
|
||||||
|
2. SDK calls "handle" function for each of the interaction.
|
||||||
|
If given interaction was modifying the state - it is updated inside the WASM module
|
||||||
|
- but not returned to host.
|
||||||
|
3. Whenever SDK needs to know the current state (eg. in order to perform
|
||||||
|
caching or to simply get its value after evaluating all of the interactions)
|
||||||
|
- it calls WASM's module "currentState" function.
|
||||||
|
|
||||||
|
The handle function by default does not return the new state -
|
||||||
|
it only updates it in the WASM module.
|
||||||
|
The handle function returns a value only in case of error
|
||||||
|
or calling a "view" function.
|
||||||
|
|
||||||
|
In the future this might also allow to enhance the inner-contracts communication
|
||||||
|
- e.g. if the execution network will store the state of the contracts - as the WASM contract module memory
|
||||||
|
- it would allow to read other contract's state "directly" from WASM module memory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// inspired by https://github.com/dfinity/examples/blob/master/rust/basic_dao/src/basic_dao/src/lib.rs#L13
|
||||||
|
thread_local! {
|
||||||
|
static STATE: RefCell<State> = RefCell::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen()]
|
||||||
|
pub async fn handle(interaction: JsValue) -> Option<JsValue> {
|
||||||
|
let result: Result<HandlerResult<State, QueryResponseMsg>, ContractError>;
|
||||||
|
let action: Result<Action, Error> = interaction.into_serde();
|
||||||
|
|
||||||
|
if action.is_err() {
|
||||||
|
// cannot pass any data from action.error here - ends up with
|
||||||
|
// "FnOnce called more than once" error from wasm-bindgen for
|
||||||
|
// "foreign_call" testcase.
|
||||||
|
result = Err(ContractError::RuntimeError(
|
||||||
|
"Error while parsing input".to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// not sure about clone here
|
||||||
|
let current_state = STATE.with(|service| service.borrow().clone());
|
||||||
|
|
||||||
|
result = contract::handle(current_state, action.unwrap()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(HandlerResult::NewState(state)) = result {
|
||||||
|
STATE.with(|service| service.replace(state));
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(JsValue::from_serde(&result).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = initState)]
|
||||||
|
pub fn init_state(state: &JsValue) {
|
||||||
|
let state_parsed: State = state.into_serde().unwrap();
|
||||||
|
|
||||||
|
STATE.with(|service| service.replace(state_parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = currentState)]
|
||||||
|
pub fn current_state() -> JsValue {
|
||||||
|
// not sure if that's deterministic - which is very important for the execution network.
|
||||||
|
// TODO: perf - according to docs:
|
||||||
|
// "This is unlikely to be super speedy so it's not recommended for large payload"
|
||||||
|
// - we should minimize calls to from_serde
|
||||||
|
let current_state = STATE.with(|service| service.borrow().clone());
|
||||||
|
JsValue::from_serde(¤t_state).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen()]
|
||||||
|
pub fn version() -> i32 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for now to simplify type reading without as/loader or wasm-bindgen
|
||||||
|
// 1 = assemblyscript
|
||||||
|
// 2 = rust
|
||||||
|
// 3 = go
|
||||||
|
// 4 = swift
|
||||||
|
// 5 = c
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn lang() -> i32 {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/////////////////////////////////////////////////////
|
||||||
|
/////////////// DO NOT MODIFY THIS FILE /////////////
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub mod entrypoint;
|
||||||
10
src/__tests__/integration/data/wrc-20/src/error.rs
Normal file
10
src/__tests__/integration/data/wrc-20/src/error.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub enum ContractError {
|
||||||
|
RuntimeError(String),
|
||||||
|
CallerBalanceNotEnough(u64),
|
||||||
|
CallerAllowanceNotEnough(u64),
|
||||||
|
OnlyOwnerCanEvolve,
|
||||||
|
EvolveNotAllowed
|
||||||
|
}
|
||||||
6
src/__tests__/integration/data/wrc-20/src/lib.rs
Normal file
6
src/__tests__/integration/data/wrc-20/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod state;
|
||||||
|
mod action;
|
||||||
|
mod error;
|
||||||
|
mod actions;
|
||||||
|
mod contract;
|
||||||
|
pub mod contract_utils;
|
||||||
18
src/__tests__/integration/data/wrc-20/src/state.rs
Normal file
18
src/__tests__/integration/data/wrc-20/src/state.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct State {
|
||||||
|
pub symbol: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub decimals: u8,
|
||||||
|
pub total_supply: u64,
|
||||||
|
pub balances: HashMap<String, u64>,
|
||||||
|
pub allowances: HashMap<String, HashMap<String, u64>>,
|
||||||
|
|
||||||
|
//Evolve interface
|
||||||
|
pub owner: String,
|
||||||
|
pub evolve: Option<String>,
|
||||||
|
pub can_evolve: Option<bool>
|
||||||
|
}
|
||||||
@@ -216,7 +216,7 @@ describe('Testing internal writes', () => {
|
|||||||
expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060);
|
expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060);
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('should properly evaluate state with a new client', async () => {
|
it('should properly evaluate state with a new client', async () => {
|
||||||
const contractA2 = WarpFactory.forLocal(port)
|
const contractA2 = WarpFactory.forLocal(port)
|
||||||
.contract<any>(contractATxId)
|
.contract<any>(contractATxId)
|
||||||
.setEvaluationOptions({ internalWrites: true })
|
.setEvaluationOptions({ internalWrites: true })
|
||||||
@@ -279,7 +279,7 @@ describe('Testing internal writes', () => {
|
|||||||
expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060);
|
expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060);
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('should properly evaluate state with a new client', async () => {
|
it('should properly evaluate state with a new client', async () => {
|
||||||
const contractA2 = WarpFactory.forLocal(port)
|
const contractA2 = WarpFactory.forLocal(port)
|
||||||
.contract<any>(contractATxId)
|
.contract<any>(contractATxId)
|
||||||
.setEvaluationOptions({ internalWrites: true })
|
.setEvaluationOptions({ internalWrites: true })
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import path from 'path';
|
||||||
|
import {mineBlock} from '../_helpers';
|
||||||
|
import {WarpFactory} from '../../../core/WarpFactory';
|
||||||
|
import {LoggerFactory} from '../../../logging/LoggerFactory';
|
||||||
|
|
||||||
|
const PORT = 1970;
|
||||||
|
|
||||||
|
var simpleThetharTxId;
|
||||||
|
var arlocal, arweave, warp, walletJwk;
|
||||||
|
var erc20Contract, simpleThetarContract;
|
||||||
|
|
||||||
|
describe('flow with broken behaviour', () => {
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// note: each tests suit (i.e. file with tests that Jest is running concurrently
|
||||||
|
// with another files has to have ArLocal set to a different port!)
|
||||||
|
arlocal = new ArLocal(PORT, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('error');
|
||||||
|
|
||||||
|
warp = WarpFactory.forLocal(PORT);
|
||||||
|
({jwk: walletJwk} = await warp.generateWallet());
|
||||||
|
arweave = warp.arweave;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deployJS = async () => {
|
||||||
|
const walletAddress = await arweave.wallets.jwkToAddress(walletJwk);
|
||||||
|
|
||||||
|
// deploy TAR pst
|
||||||
|
const erc20Src = fs.readFileSync(path.join(__dirname, '../data/staking/erc-20.js'), 'utf8');
|
||||||
|
|
||||||
|
const tarInit = {
|
||||||
|
symbol: 'TAR',
|
||||||
|
name: 'ThetAR exchange token',
|
||||||
|
decimals: 2,
|
||||||
|
totalSupply: 20000,
|
||||||
|
balances: {
|
||||||
|
[walletAddress]: 10000,
|
||||||
|
},
|
||||||
|
allowances: {},
|
||||||
|
settings: null,
|
||||||
|
owner: walletAddress,
|
||||||
|
canEvolve: true,
|
||||||
|
evolve: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const erc20TxId = (await warp.createContract.deploy({
|
||||||
|
wallet: walletJwk,
|
||||||
|
initState: JSON.stringify(tarInit),
|
||||||
|
src: erc20Src,
|
||||||
|
})).contractTxId;
|
||||||
|
erc20Contract = warp.contract(erc20TxId);
|
||||||
|
erc20Contract.setEvaluationOptions({
|
||||||
|
internalWrites: true,
|
||||||
|
allowUnsafeClient: true,
|
||||||
|
allowBigInt: true,
|
||||||
|
}).connect(walletJwk);
|
||||||
|
|
||||||
|
// deploy thetAR contract
|
||||||
|
const simpleThetharSrc = fs.readFileSync(path.join(__dirname, '../data/thethar/simple-thethar-contract.js'), 'utf8');
|
||||||
|
const contractInit = {
|
||||||
|
token: erc20TxId,
|
||||||
|
orders: []
|
||||||
|
};
|
||||||
|
|
||||||
|
simpleThetharTxId = (await warp.createContract.deploy({
|
||||||
|
wallet: walletJwk,
|
||||||
|
initState: JSON.stringify(contractInit),
|
||||||
|
src: simpleThetharSrc,
|
||||||
|
})).contractTxId;
|
||||||
|
simpleThetarContract = warp.contract(simpleThetharTxId);
|
||||||
|
simpleThetarContract.setEvaluationOptions({
|
||||||
|
internalWrites: true,
|
||||||
|
allowUnsafeClient: true,
|
||||||
|
allowBigInt: true,
|
||||||
|
}).connect(walletJwk);
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (quantity) => {
|
||||||
|
await erc20Contract.writeInteraction({
|
||||||
|
function: 'approve',
|
||||||
|
spender: simpleThetharTxId,
|
||||||
|
amount: quantity
|
||||||
|
});
|
||||||
|
|
||||||
|
await mineBlock(warp);
|
||||||
|
|
||||||
|
const txId = (await simpleThetarContract.writeInteraction({
|
||||||
|
function: 'create'
|
||||||
|
})).originalTxId;
|
||||||
|
|
||||||
|
await mineBlock(warp);
|
||||||
|
|
||||||
|
console.log('AFTER: ', JSON.stringify(await simpleThetarContract.readState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = async (orderId) => {
|
||||||
|
console.log('cancel order...');
|
||||||
|
|
||||||
|
const txId = await simpleThetarContract.writeInteraction({
|
||||||
|
function: 'cancel',
|
||||||
|
params: {
|
||||||
|
orderId: orderId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await mineBlock(warp);
|
||||||
|
|
||||||
|
console.log('AFTER: ', JSON.stringify(await simpleThetarContract.readState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const readFull = async () => {
|
||||||
|
const warp = WarpFactory.forLocal(PORT);
|
||||||
|
|
||||||
|
let contract = warp.contract(simpleThetharTxId);
|
||||||
|
contract.setEvaluationOptions({
|
||||||
|
internalWrites: true,
|
||||||
|
allowUnsafeClient: true,
|
||||||
|
allowBigInt: true
|
||||||
|
}).connect(walletJwk);
|
||||||
|
|
||||||
|
const result = await contract.readState();
|
||||||
|
|
||||||
|
console.log('Contract: ', JSON.stringify(result, null, " "));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('correctly evaluate deferred state', async () => {
|
||||||
|
await deployJS();
|
||||||
|
await create(1);
|
||||||
|
await cancel(0);
|
||||||
|
|
||||||
|
console.error("========= READ FULL ==========")
|
||||||
|
const result = await readFull();
|
||||||
|
expect(result.cachedValue.state.orders.length).toEqual(0);
|
||||||
|
|
||||||
|
const errorMessages = result.cachedValue.errorMessages;
|
||||||
|
for (let errorMessageKey in errorMessages) {
|
||||||
|
expect(errorMessages[errorMessageKey]).not.toContain('TransferFromZero');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -52,7 +52,8 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
|
|||||||
console.log('readState', contractTxId);
|
console.log('readState', contractTxId);
|
||||||
try {
|
try {
|
||||||
console.log = function () {}; // to hide any logs from contracts...
|
console.log = function () {}; // to hide any logs from contracts...
|
||||||
const result2 = await WarpFactory.custom(
|
|
||||||
|
const warp = WarpFactory.custom(
|
||||||
arweave,
|
arweave,
|
||||||
{
|
{
|
||||||
...defaultCacheOptions,
|
...defaultCacheOptions,
|
||||||
@@ -67,7 +68,9 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
|
|||||||
inMemory: true
|
inMemory: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build();
|
||||||
|
|
||||||
|
const result2 = await warp
|
||||||
.contract(contractTxId)
|
.contract(contractTxId)
|
||||||
.setEvaluationOptions({
|
.setEvaluationOptions({
|
||||||
unsafeClient: 'allow',
|
unsafeClient: 'allow',
|
||||||
@@ -75,9 +78,15 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
|
|||||||
})
|
})
|
||||||
.readState(blockHeight);
|
.readState(blockHeight);
|
||||||
const result2String = stringify(result2.cachedValue.state).trim();
|
const result2String = stringify(result2.cachedValue.state).trim();
|
||||||
|
|
||||||
|
await warp.stateEvaluator.getCache().prune(1);
|
||||||
|
|
||||||
expect(result2String).toEqual(resultString);
|
expect(result2String).toEqual(resultString);
|
||||||
} finally {
|
} finally {
|
||||||
console.log = originalConsoleLog;
|
console.log = originalConsoleLog;
|
||||||
|
const heap = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100;
|
||||||
|
const rss = Math.round((process.memoryUsage().rss / 1024 / 1024) * 100) / 100;
|
||||||
|
console.log('Memory', { heap, rss });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
800000
|
800000
|
||||||
@@ -94,7 +103,7 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
|
|||||||
.readFileSync(path.join(__dirname, 'test-cases', 'contracts', `${contractTxId}.json`), 'utf-8')
|
.readFileSync(path.join(__dirname, 'test-cases', 'contracts', `${contractTxId}.json`), 'utf-8')
|
||||||
.trim();
|
.trim();
|
||||||
console.log('readState', contractTxId);
|
console.log('readState', contractTxId);
|
||||||
const result2 = await WarpFactory.custom(
|
const warp = WarpFactory.custom(
|
||||||
arweave,
|
arweave,
|
||||||
{
|
{
|
||||||
...defaultCacheOptions,
|
...defaultCacheOptions,
|
||||||
@@ -109,14 +118,15 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
|
|||||||
inMemory: true
|
inMemory: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build();
|
||||||
.contract(contractTxId)
|
|
||||||
|
const result2 = await warp.contract(contractTxId)
|
||||||
.setEvaluationOptions({
|
.setEvaluationOptions({
|
||||||
useVM2: true,
|
useVM2: true,
|
||||||
unsafeClient: 'allow',
|
unsafeClient: 'allow',
|
||||||
allowBigInt: true
|
allowBigInt: true
|
||||||
})
|
}).readState(blockHeight);
|
||||||
.readState(blockHeight);
|
|
||||||
const result2String = stringify(result2.cachedValue.state).trim();
|
const result2String = stringify(result2.cachedValue.state).trim();
|
||||||
expect(result2String).toEqual(resultString);
|
expect(result2String).toEqual(resultString);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export interface Contract<State = unknown> {
|
|||||||
*/
|
*/
|
||||||
readState(
|
readState(
|
||||||
sortKeyOrBlockHeight?: string | number,
|
sortKeyOrBlockHeight?: string | number,
|
||||||
|
caller?: string,
|
||||||
interactions?: GQLNodeInterface[]
|
interactions?: GQLNodeInterface[]
|
||||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>>;
|
): Promise<SortKeyCacheResult<EvalStateResult<State>>>;
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ export interface Contract<State = unknown> {
|
|||||||
transfer?: ArTransfer
|
transfer?: ArTransfer
|
||||||
): Promise<InteractionResult<State, unknown>>;
|
): Promise<InteractionResult<State, unknown>>;
|
||||||
|
|
||||||
dryWriteFromTx<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>>;
|
applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a new "interaction" transaction - i.e. such transaction that stores input for the contract.
|
* Writes a new "interaction" transaction - i.e. such transaction that stores input for the contract.
|
||||||
@@ -234,4 +235,8 @@ export interface Contract<State = unknown> {
|
|||||||
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
|
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
|
||||||
|
|
||||||
hasUncommittedState(contractTxId: string): boolean;
|
hasUncommittedState(contractTxId: string): boolean;
|
||||||
|
|
||||||
|
resetUncommittedState(): void;
|
||||||
|
|
||||||
|
commitStates(interaction: GQLNodeInterface): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,7 @@ import { LoggerFactory } from '../logging/LoggerFactory';
|
|||||||
import { Evolve } from '../plugins/Evolve';
|
import { Evolve } from '../plugins/Evolve';
|
||||||
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
|
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
|
||||||
import { sleep } from '../utils/utils';
|
import { sleep } from '../utils/utils';
|
||||||
import {
|
import { BenchmarkStats, Contract, InnerCallData, WriteInteractionOptions, WriteInteractionResponse } from './Contract';
|
||||||
BenchmarkStats,
|
|
||||||
Contract,
|
|
||||||
CurrentTx,
|
|
||||||
InnerCallData,
|
|
||||||
WriteInteractionOptions,
|
|
||||||
WriteInteractionResponse
|
|
||||||
} from './Contract';
|
|
||||||
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
|
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
|
||||||
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
|
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
|
||||||
import { generateMockVrf } from '../utils/vrf';
|
import { generateMockVrf } from '../utils/vrf';
|
||||||
@@ -37,6 +30,7 @@ import { Signature, CustomSignature } from './Signature';
|
|||||||
import { ContractDefinition } from '../core/ContractDefinition';
|
import { ContractDefinition } from '../core/ContractDefinition';
|
||||||
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
|
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
|
||||||
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
|
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
|
||||||
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link Contract} that is backwards compatible with current style
|
* An implementation of {@link Contract} that is backwards compatible with current style
|
||||||
@@ -66,6 +60,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
|
|
||||||
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
|
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
|
||||||
|
|
||||||
|
private readonly mutex = new Mutex();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _contractTxId: string,
|
private readonly _contractTxId: string,
|
||||||
protected readonly warp: Warp,
|
protected readonly warp: Warp,
|
||||||
@@ -129,25 +125,37 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
|
|
||||||
async readState(
|
async readState(
|
||||||
sortKeyOrBlockHeight?: string | number,
|
sortKeyOrBlockHeight?: string | number,
|
||||||
|
caller?: string,
|
||||||
interactions?: GQLNodeInterface[]
|
interactions?: GQLNodeInterface[]
|
||||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||||
this.logger.info('Read state for', {
|
this.logger.info('Read state for', {
|
||||||
contractTxId: this._contractTxId,
|
contractTxId: this._contractTxId,
|
||||||
sortKeyOrBlockHeight
|
sortKeyOrBlockHeight
|
||||||
});
|
});
|
||||||
const initBenchmark = Benchmark.measure();
|
|
||||||
this.maybeResetRootContract();
|
|
||||||
if (!this.isRoot() && sortKeyOrBlockHeight == null) {
|
if (!this.isRoot() && sortKeyOrBlockHeight == null) {
|
||||||
throw new Error('SortKey MUST be always set for non-root contract calls');
|
throw new Error('SortKey MUST be always set for non-root contract calls');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stateEvaluator } = this.warp;
|
const { stateEvaluator } = this.warp;
|
||||||
|
|
||||||
const sortKey =
|
const sortKey =
|
||||||
typeof sortKeyOrBlockHeight == 'number'
|
typeof sortKeyOrBlockHeight == 'number'
|
||||||
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
|
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
|
||||||
: sortKeyOrBlockHeight;
|
: sortKeyOrBlockHeight;
|
||||||
|
|
||||||
|
if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) {
|
||||||
|
const result = this.getUncommittedState(this.txId());
|
||||||
|
return {
|
||||||
|
sortKey,
|
||||||
|
cachedValue: result as EvalStateResult<State>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: not sure if we should synchronize on a contract instance or contractTxId
|
||||||
|
// in the latter case, the warp instance should keep a map contractTxId -> mutex
|
||||||
|
const releaseMutex = await this.mutex.acquire();
|
||||||
|
try {
|
||||||
|
const initBenchmark = Benchmark.measure();
|
||||||
|
this.maybeResetRootContract();
|
||||||
|
|
||||||
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
|
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
|
||||||
this.logger.info('Execution Context', {
|
this.logger.info('Execution Context', {
|
||||||
srcTxId: executionContext.contractDefinition?.srcTxId,
|
srcTxId: executionContext.contractDefinition?.srcTxId,
|
||||||
@@ -174,7 +182,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
'Total: ': `${total.toFixed(0)}ms`
|
'Total: ': `${total.toFixed(0)}ms`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (sortKey && !this.isRoot()) {
|
||||||
|
this.setUncommittedState(this.txId(), result.cachedValue);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
} finally {
|
||||||
|
releaseMutex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readStateFor(
|
async readStateFor(
|
||||||
@@ -198,7 +213,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
interactionTx: GQLNodeInterface
|
interactionTx: GQLNodeInterface
|
||||||
): Promise<InteractionResult<State, View>> {
|
): Promise<InteractionResult<State, View>> {
|
||||||
this.logger.info(`View state for ${this._contractTxId}`, interactionTx);
|
this.logger.info(`View state for ${this._contractTxId}`, interactionTx);
|
||||||
return await this.callContractForTx<Input, View>(input, interactionTx);
|
return await this.doApplyInputOnTx<Input, View>(input, interactionTx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dryWrite<Input>(
|
async dryWrite<Input>(
|
||||||
@@ -211,9 +226,9 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
return await this.callContract<Input>(input, caller, undefined, tags, transfer);
|
return await this.callContract<Input>(input, caller, undefined, tags, transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dryWriteFromTx<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
|
async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
|
||||||
this.logger.info(`Dry-write from transaction ${transaction.id} for ${this._contractTxId}`);
|
this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
|
||||||
return await this.callContractForTx<Input>(input, transaction);
|
return await this.doApplyInputOnTx<Input>(input, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeInteraction<Input>(
|
async writeInteraction<Input>(
|
||||||
@@ -683,14 +698,25 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
return handleResult;
|
return handleResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async callContractForTx<Input, View = unknown>(
|
private async doApplyInputOnTx<Input, View = unknown>(
|
||||||
input: Input,
|
input: Input,
|
||||||
interactionTx: GQLNodeInterface
|
interactionTx: GQLNodeInterface
|
||||||
): Promise<InteractionResult<State, View>> {
|
): Promise<InteractionResult<State, View>> {
|
||||||
this.maybeResetRootContract();
|
this.maybeResetRootContract();
|
||||||
|
|
||||||
|
let evalStateResult: SortKeyCacheResult<EvalStateResult<State>>;
|
||||||
|
|
||||||
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
|
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
|
||||||
const evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
|
|
||||||
|
if (!this.isRoot() && this.hasUncommittedState(this.txId())) {
|
||||||
|
evalStateResult = {
|
||||||
|
sortKey: interactionTx.sortKey,
|
||||||
|
cachedValue: this.getUncommittedState(this.txId()) as EvalStateResult<State>
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
|
||||||
|
this.setUncommittedState(this.txId(), evalStateResult.cachedValue);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug('callContractForTx - evalStateResult', {
|
this.logger.debug('callContractForTx - evalStateResult', {
|
||||||
result: evalStateResult.cachedValue.state,
|
result: evalStateResult.cachedValue.state,
|
||||||
@@ -803,15 +829,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
return this._rootSortKey;
|
return this._rootSortKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRoot(): Contract<unknown> {
|
|
||||||
let result: Contract = this;
|
|
||||||
while (!result.isRoot()) {
|
|
||||||
result = result.parent();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEoEvaluator(): EvaluationOptionsEvaluator {
|
getEoEvaluator(): EvaluationOptionsEvaluator {
|
||||||
const root = this.getRoot() as HandlerBasedContract<unknown>;
|
const root = this.getRoot() as HandlerBasedContract<unknown>;
|
||||||
return root._eoEvaluator;
|
return root._eoEvaluator;
|
||||||
@@ -850,10 +867,38 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
|
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
|
||||||
return (this.getRoot() as HandlerBasedContract<unknown>)._uncommittedStates.get(contractTxId);
|
return (this.getRoot() as HandlerBasedContract<unknown>)._uncommittedStates.get(contractTxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void {
|
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void {
|
||||||
(this.getRoot() as HandlerBasedContract<unknown>)._uncommittedStates.set(contractTxId, result);
|
this.getRoot()._uncommittedStates.set(contractTxId, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUncommittedState(contractTxId: string): boolean {
|
hasUncommittedState(contractTxId: string): boolean {
|
||||||
return (this.getRoot() as HandlerBasedContract<unknown>)._uncommittedStates.has(contractTxId);
|
return this.getRoot()._uncommittedStates.has(contractTxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetUncommittedState(): void {
|
||||||
|
this.getRoot()._uncommittedStates = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitStates(interaction: GQLNodeInterface): Promise<void> {
|
||||||
|
const uncommittedStates = this.getRoot()._uncommittedStates;
|
||||||
|
try {
|
||||||
|
if (uncommittedStates.size > 1) {
|
||||||
|
for (const [k, v] of uncommittedStates) {
|
||||||
|
await this.warp.stateEvaluator.putInCache(k, interaction, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.resetUncommittedState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRoot(): HandlerBasedContract<unknown> {
|
||||||
|
let result: Contract = this;
|
||||||
|
while (!result.isRoot()) {
|
||||||
|
result = result.parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as HandlerBasedContract<unknown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export type ExecutionContext<State, Api = unknown> = {
|
|||||||
* performs all the computation.
|
* performs all the computation.
|
||||||
*/
|
*/
|
||||||
handler: Api;
|
handler: Api;
|
||||||
caller?: string; // note: this is only set for "viewState" operations
|
caller?: string; // note: this is only set for "viewState" and "write" operations
|
||||||
cachedState?: SortKeyCacheResult<EvalStateResult<State>>;
|
cachedState?: SortKeyCacheResult<EvalStateResult<State>>;
|
||||||
requestedSortKey?: string;
|
requestedSortKey?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const missingInteractions = executionContext.sortedInteractions;
|
const missingInteractions = executionContext.sortedInteractions;
|
||||||
|
|
||||||
// TODO: this is tricky part, needs proper description
|
|
||||||
// for now: it prevents from infinite loop calls between calls that are making
|
|
||||||
// internal interact writes.
|
|
||||||
const contractTxId = executionContext.contractDefinition.txId;
|
const contractTxId = executionContext.contractDefinition.txId;
|
||||||
// sanity check...
|
// sanity check...
|
||||||
if (!contractTxId) {
|
if (!contractTxId) {
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
if (shouldBreakAfterEvolve) {
|
if (shouldBreakAfterEvolve) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||||
|
|
||||||
const missingInteraction = missingInteractions[i];
|
const missingInteraction = missingInteractions[i];
|
||||||
const singleInteractionBenchmark = Benchmark.measure();
|
const singleInteractionBenchmark = Benchmark.measure();
|
||||||
currentSortKey = missingInteraction.sortKey;
|
currentSortKey = missingInteraction.sortKey;
|
||||||
@@ -125,7 +128,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);
|
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);
|
||||||
|
|
||||||
// other contract makes write ("writing contract") on THIS contract
|
// other contract makes write ("writing contract") on THIS contract
|
||||||
if (isInteractWrite && internalWrites) {
|
if (isInteractWrite && internalWrites) {
|
||||||
// evaluating txId of the contract that is writing on THIS contract
|
// evaluating txId of the contract that is writing on THIS contract
|
||||||
@@ -137,27 +139,21 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
.addInteractionData({ interaction: null, interactionTx: missingInteraction });
|
.addInteractionData({ interaction: null, interactionTx: missingInteraction });
|
||||||
|
|
||||||
// creating a Contract instance for the "writing" contract
|
// creating a Contract instance for the "writing" contract
|
||||||
const writingContract = executionContext.warp.contract(writingContractTxId, executionContext.contract, {
|
const writingContract = warp.contract(writingContractTxId, executionContext.contract, {
|
||||||
callingInteraction: missingInteraction,
|
callingInteraction: missingInteraction,
|
||||||
callType: 'read'
|
callType: 'read'
|
||||||
});
|
});
|
||||||
|
|
||||||
/*await this.onContractCall(
|
|
||||||
missingInteraction,
|
|
||||||
executionContext,
|
|
||||||
new EvalStateResult<State>(currentState, validity, errorMessages)
|
|
||||||
);*/
|
|
||||||
|
|
||||||
this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, missingInteraction.sortKey);
|
this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, missingInteraction.sortKey);
|
||||||
/**
|
/**
|
||||||
Reading the state of the writing contract.
|
Reading the state of the writing contract.
|
||||||
This in turn will cause the state of THIS contract to be
|
This in turn will cause the state of THIS contract to be
|
||||||
updated in cache - see {@link ContractHandlerApi.assignWrite}
|
updated in uncommitted state
|
||||||
*/
|
*/
|
||||||
let newState = null;
|
let newState: EvalStateResult<unknown> = null;
|
||||||
try {
|
try {
|
||||||
await writingContract.readState(missingInteraction.sortKey);
|
await writingContract.readState(missingInteraction.sortKey);
|
||||||
//newState = await this.internalWriteState<State>(contractDefinition.txId, missingInteraction.sortKey);
|
newState = contract.getUncommittedState(contract.txId());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
|
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
|
||||||
this.logger.warn('Skipping unsafe contract in internal write');
|
this.logger.warn('Skipping unsafe contract in internal write');
|
||||||
@@ -176,17 +172,16 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
|
|
||||||
// loading latest state of THIS contract from cache
|
// loading latest state of THIS contract from cache
|
||||||
if (newState !== null) {
|
if (newState !== null) {
|
||||||
currentState = newState.cachedValue.state;
|
currentState = newState.state as State;
|
||||||
// we need to update the state in the wasm module
|
// we need to update the state in the wasm module
|
||||||
executionContext?.handler.initState(currentState);
|
executionContext?.handler.initState(currentState);
|
||||||
|
|
||||||
validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id];
|
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
|
||||||
if (newState.cachedValue.errorMessages?.[missingInteraction.id]) {
|
if (newState.errorMessages?.[missingInteraction.id]) {
|
||||||
errorMessages[missingInteraction.id] = newState.cachedValue.errorMessages[missingInteraction.id];
|
errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
||||||
await this.onStateUpdate<State>(missingInteraction, executionContext, toCache);
|
|
||||||
if (canBeCached(missingInteraction)) {
|
if (canBeCached(missingInteraction)) {
|
||||||
lastConfirmedTxState = {
|
lastConfirmedTxState = {
|
||||||
tx: missingInteraction,
|
tx: missingInteraction,
|
||||||
@@ -267,12 +262,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
state: toCache
|
state: toCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await this.onStateUpdate<State>(
|
|
||||||
missingInteraction,
|
|
||||||
executionContext,
|
|
||||||
toCache,
|
|
||||||
cacheEveryNInteractions % i == 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressPlugin) {
|
if (progressPlugin) {
|
||||||
@@ -297,11 +286,17 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contract.isRoot()) {
|
||||||
|
await contract.commitStates(missingInteraction);
|
||||||
|
} else {
|
||||||
|
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const evalStateResult = new EvalStateResult<State>(currentState, validity, errorMessages);
|
const evalStateResult = new EvalStateResult<State>(currentState, validity, errorMessages);
|
||||||
|
|
||||||
// state could have been fully retrieved from cache
|
// state could have been fully retrieved from cache
|
||||||
// or there were no interactions below requested block height
|
// or there were no interactions below requested sort key
|
||||||
if (lastConfirmedTxState !== null) {
|
if (lastConfirmedTxState !== null) {
|
||||||
await this.onStateEvaluated(lastConfirmedTxState.tx, executionContext, lastConfirmedTxState.state);
|
await this.onStateEvaluated(lastConfirmedTxState.tx, executionContext, lastConfirmedTxState.state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
callingInteraction: this.swGlobal._activeTx,
|
callingInteraction: this.swGlobal._activeTx,
|
||||||
callType: 'write'
|
callType: 'write'
|
||||||
});
|
});
|
||||||
const result = await calleeContract.dryWriteFromTx<Input>(input, this.swGlobal._activeTx);
|
const result = await calleeContract.applyInput<Input>(input, this.swGlobal._activeTx);
|
||||||
|
|
||||||
this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry);
|
this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry);
|
||||||
const shouldAutoThrow =
|
const shouldAutoThrow =
|
||||||
@@ -69,7 +69,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
? `Internal write auto error for call [${JSON.stringify(debugData)}]: ${result.errorMessage}`
|
? `Internal write auto error for call [${JSON.stringify(debugData)}]: ${result.errorMessage}`
|
||||||
: result.errorMessage;
|
: result.errorMessage;
|
||||||
|
|
||||||
await executionContext.warp.stateEvaluator.onInternalWriteStateUpdate(this.swGlobal._activeTx, contractTxId, {
|
calleeContract.setUncommittedState(calleeContract.txId(), {
|
||||||
state: result.state as State,
|
state: result.state as State,
|
||||||
validity: {
|
validity: {
|
||||||
...result.originalValidity,
|
...result.originalValidity,
|
||||||
@@ -80,6 +80,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
[this.swGlobal._activeTx.id]: effectiveErrorMessage
|
[this.swGlobal._activeTx.id]: effectiveErrorMessage
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldAutoThrow) {
|
if (shouldAutoThrow) {
|
||||||
throw new ContractError(effectiveErrorMessage);
|
throw new ContractError(effectiveErrorMessage);
|
||||||
}
|
}
|
||||||
@@ -117,20 +118,27 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
transaction: this.swGlobal.transaction.id
|
transaction: this.swGlobal.transaction.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const {contract} = executionContext;
|
const { contract, warp } = executionContext;
|
||||||
|
|
||||||
let stateWithValidity;
|
/*let stateWithValidity;
|
||||||
if (!contract.isRoot() && contract.hasUncommittedState(contractTxId)) {
|
if (!contract.isRoot() && contract.hasUncommittedState(contractTxId)) {
|
||||||
stateWithValidity = contract.getUncommittedState(contractTxId);
|
stateWithValidity = contract.getUncommittedState(contractTxId);
|
||||||
} else {
|
} else {
|
||||||
const childContract = executionContext.warp.contract(contractTxId, contract, {
|
const childContract = warp.contract(contractTxId, contract, {
|
||||||
callingInteraction: interactionTx,
|
callingInteraction: interactionTx,
|
||||||
callType: 'read'
|
callType: 'read'
|
||||||
});
|
});
|
||||||
|
|
||||||
stateWithValidity = await childContract.readState(interactionTx.sortKey);
|
stateWithValidity = await childContract.readState(interactionTx.sortKey);
|
||||||
executionContext.contract.setUncommittedState(contractTxId, stateWithValidity);
|
childContract.setUncommittedState(contractTxId, stateWithValidity);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
const childContract = warp.contract(contractTxId, contract, {
|
||||||
|
callingInteraction: interactionTx,
|
||||||
|
callType: 'read'
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateWithValidity = await childContract.readState(interactionTx.sortKey);
|
||||||
|
|
||||||
if (stateWithValidity?.cachedValue?.errorMessages) {
|
if (stateWithValidity?.cachedValue?.errorMessages) {
|
||||||
const errorKeys = Reflect.ownKeys(stateWithValidity?.cachedValue?.errorMessages);
|
const errorKeys = Reflect.ownKeys(stateWithValidity?.cachedValue?.errorMessages);
|
||||||
@@ -160,12 +168,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
|||||||
|
|
||||||
protected assignRefreshState(executionContext: ExecutionContext<State>) {
|
protected assignRefreshState(executionContext: ExecutionContext<State>) {
|
||||||
this.swGlobal.contracts.refreshState = async () => {
|
this.swGlobal.contracts.refreshState = async () => {
|
||||||
const stateEvaluator = executionContext.warp.stateEvaluator;
|
return executionContext.contract.getUncommittedState(this.swGlobal.contract.id)?.state;
|
||||||
const result = await stateEvaluator.latestAvailableState(
|
|
||||||
this.swGlobal.contract.id,
|
|
||||||
this.swGlobal._activeTx.sortKey
|
|
||||||
);
|
|
||||||
return result?.cachedValue.state;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2161,6 +2161,13 @@ astral-regex@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||||
|
|
||||||
|
async-mutex@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f"
|
||||||
|
integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
async-retry@^1.2.1:
|
async-retry@^1.2.1:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
|
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
|
||||||
|
|||||||
Reference in New Issue
Block a user