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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install modules
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"@idena/vrf-js": "^1.0.1",
|
||||
"archiver": "^5.3.0",
|
||||
"arweave": "1.11.8",
|
||||
"async-mutex": "^0.4.0",
|
||||
"elliptic": "^6.5.4",
|
||||
"events": "3.3.0",
|
||||
"fast-copy": "^3.0.0",
|
||||
|
||||
@@ -73,6 +73,10 @@ export function handle(state, action) {
|
||||
const recipient = _input.recipient;
|
||||
const amount = _input.amount;
|
||||
|
||||
if (amount == 0 ) {
|
||||
throw new ContractError('TransferFromZero');
|
||||
}
|
||||
|
||||
const currentAllowance = _allowances[sender][_msgSender];
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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)
|
||||
.contract<any>(contractATxId)
|
||||
.setEvaluationOptions({ internalWrites: true })
|
||||
@@ -279,7 +279,7 @@ describe('Testing internal writes', () => {
|
||||
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)
|
||||
.contract<any>(contractATxId)
|
||||
.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);
|
||||
try {
|
||||
console.log = function () {}; // to hide any logs from contracts...
|
||||
const result2 = await WarpFactory.custom(
|
||||
|
||||
const warp = WarpFactory.custom(
|
||||
arweave,
|
||||
{
|
||||
...defaultCacheOptions,
|
||||
@@ -67,7 +68,9 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
|
||||
inMemory: true
|
||||
}
|
||||
)
|
||||
.build()
|
||||
.build();
|
||||
|
||||
const result2 = await warp
|
||||
.contract(contractTxId)
|
||||
.setEvaluationOptions({
|
||||
unsafeClient: 'allow',
|
||||
@@ -75,9 +78,15 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
|
||||
})
|
||||
.readState(blockHeight);
|
||||
const result2String = stringify(result2.cachedValue.state).trim();
|
||||
|
||||
await warp.stateEvaluator.getCache().prune(1);
|
||||
|
||||
expect(result2String).toEqual(resultString);
|
||||
} finally {
|
||||
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
|
||||
@@ -94,7 +103,7 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
|
||||
.readFileSync(path.join(__dirname, 'test-cases', 'contracts', `${contractTxId}.json`), 'utf-8')
|
||||
.trim();
|
||||
console.log('readState', contractTxId);
|
||||
const result2 = await WarpFactory.custom(
|
||||
const warp = WarpFactory.custom(
|
||||
arweave,
|
||||
{
|
||||
...defaultCacheOptions,
|
||||
@@ -109,14 +118,15 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
|
||||
inMemory: true
|
||||
}
|
||||
)
|
||||
.build()
|
||||
.contract(contractTxId)
|
||||
.build();
|
||||
|
||||
const result2 = await warp.contract(contractTxId)
|
||||
.setEvaluationOptions({
|
||||
useVM2: true,
|
||||
unsafeClient: 'allow',
|
||||
allowBigInt: true
|
||||
})
|
||||
.readState(blockHeight);
|
||||
}).readState(blockHeight);
|
||||
|
||||
const result2String = stringify(result2.cachedValue.state).trim();
|
||||
expect(result2String).toEqual(resultString);
|
||||
},
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface Contract<State = unknown> {
|
||||
*/
|
||||
readState(
|
||||
sortKeyOrBlockHeight?: string | number,
|
||||
caller?: string,
|
||||
interactions?: GQLNodeInterface[]
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>>;
|
||||
|
||||
@@ -155,7 +156,7 @@ export interface Contract<State = unknown> {
|
||||
transfer?: ArTransfer
|
||||
): 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.
|
||||
@@ -234,4 +235,8 @@ export interface Contract<State = unknown> {
|
||||
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
|
||||
|
||||
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 { ArweaveWrapper } from '../utils/ArweaveWrapper';
|
||||
import { sleep } from '../utils/utils';
|
||||
import {
|
||||
BenchmarkStats,
|
||||
Contract,
|
||||
CurrentTx,
|
||||
InnerCallData,
|
||||
WriteInteractionOptions,
|
||||
WriteInteractionResponse
|
||||
} from './Contract';
|
||||
import { BenchmarkStats, Contract, InnerCallData, WriteInteractionOptions, WriteInteractionResponse } from './Contract';
|
||||
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
|
||||
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
|
||||
import { generateMockVrf } from '../utils/vrf';
|
||||
@@ -37,6 +30,7 @@ import { Signature, CustomSignature } from './Signature';
|
||||
import { ContractDefinition } from '../core/ContractDefinition';
|
||||
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
|
||||
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
/**
|
||||
* 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 readonly mutex = new Mutex();
|
||||
|
||||
constructor(
|
||||
private readonly _contractTxId: string,
|
||||
protected readonly warp: Warp,
|
||||
@@ -129,25 +125,37 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
async readState(
|
||||
sortKeyOrBlockHeight?: string | number,
|
||||
caller?: string,
|
||||
interactions?: GQLNodeInterface[]
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||
this.logger.info('Read state for', {
|
||||
contractTxId: this._contractTxId,
|
||||
sortKeyOrBlockHeight
|
||||
});
|
||||
const initBenchmark = Benchmark.measure();
|
||||
this.maybeResetRootContract();
|
||||
if (!this.isRoot() && sortKeyOrBlockHeight == null) {
|
||||
throw new Error('SortKey MUST be always set for non-root contract calls');
|
||||
}
|
||||
|
||||
const { stateEvaluator } = this.warp;
|
||||
|
||||
const sortKey =
|
||||
typeof sortKeyOrBlockHeight == 'number'
|
||||
? this._sorter.generateLastSortKey(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);
|
||||
this.logger.info('Execution Context', {
|
||||
srcTxId: executionContext.contractDefinition?.srcTxId,
|
||||
@@ -174,7 +182,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
'Total: ': `${total.toFixed(0)}ms`
|
||||
});
|
||||
|
||||
if (sortKey && !this.isRoot()) {
|
||||
this.setUncommittedState(this.txId(), result.cachedValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
releaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
async readStateFor(
|
||||
@@ -198,7 +213,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
interactionTx: GQLNodeInterface
|
||||
): Promise<InteractionResult<State, View>> {
|
||||
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>(
|
||||
@@ -211,9 +226,9 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
return await this.callContract<Input>(input, caller, undefined, tags, transfer);
|
||||
}
|
||||
|
||||
async dryWriteFromTx<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
|
||||
this.logger.info(`Dry-write from transaction ${transaction.id} for ${this._contractTxId}`);
|
||||
return await this.callContractForTx<Input>(input, transaction);
|
||||
async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
|
||||
this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
|
||||
return await this.doApplyInputOnTx<Input>(input, transaction);
|
||||
}
|
||||
|
||||
async writeInteraction<Input>(
|
||||
@@ -683,14 +698,25 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
return handleResult;
|
||||
}
|
||||
|
||||
private async callContractForTx<Input, View = unknown>(
|
||||
private async doApplyInputOnTx<Input, View = unknown>(
|
||||
input: Input,
|
||||
interactionTx: GQLNodeInterface
|
||||
): Promise<InteractionResult<State, View>> {
|
||||
this.maybeResetRootContract();
|
||||
|
||||
let evalStateResult: SortKeyCacheResult<EvalStateResult<State>>;
|
||||
|
||||
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', {
|
||||
result: evalStateResult.cachedValue.state,
|
||||
@@ -803,15 +829,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
return this._rootSortKey;
|
||||
}
|
||||
|
||||
private getRoot(): Contract<unknown> {
|
||||
let result: Contract = this;
|
||||
while (!result.isRoot()) {
|
||||
result = result.parent();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getEoEvaluator(): EvaluationOptionsEvaluator {
|
||||
const root = this.getRoot() as HandlerBasedContract<unknown>;
|
||||
return root._eoEvaluator;
|
||||
@@ -850,10 +867,38 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
|
||||
return (this.getRoot() as HandlerBasedContract<unknown>)._uncommittedStates.get(contractTxId);
|
||||
}
|
||||
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
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>>;
|
||||
requestedSortKey?: string;
|
||||
};
|
||||
|
||||
@@ -42,10 +42,6 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
}
|
||||
|
||||
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;
|
||||
// sanity check...
|
||||
if (!contractTxId) {
|
||||
|
||||
@@ -96,6 +96,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
if (shouldBreakAfterEvolve) {
|
||||
break;
|
||||
}
|
||||
|
||||
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||
|
||||
const missingInteraction = missingInteractions[i];
|
||||
const singleInteractionBenchmark = Benchmark.measure();
|
||||
currentSortKey = missingInteraction.sortKey;
|
||||
@@ -125,7 +128,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
);
|
||||
|
||||
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);
|
||||
|
||||
// other contract makes write ("writing contract") on THIS contract
|
||||
if (isInteractWrite && internalWrites) {
|
||||
// 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 });
|
||||
|
||||
// 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,
|
||||
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);
|
||||
/**
|
||||
Reading the state of the writing contract.
|
||||
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 {
|
||||
await writingContract.readState(missingInteraction.sortKey);
|
||||
//newState = await this.internalWriteState<State>(contractDefinition.txId, missingInteraction.sortKey);
|
||||
newState = contract.getUncommittedState(contract.txId());
|
||||
} catch (e) {
|
||||
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
|
||||
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
|
||||
if (newState !== null) {
|
||||
currentState = newState.cachedValue.state;
|
||||
currentState = newState.state as State;
|
||||
// we need to update the state in the wasm module
|
||||
executionContext?.handler.initState(currentState);
|
||||
|
||||
validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id];
|
||||
if (newState.cachedValue.errorMessages?.[missingInteraction.id]) {
|
||||
errorMessages[missingInteraction.id] = newState.cachedValue.errorMessages[missingInteraction.id];
|
||||
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
|
||||
if (newState.errorMessages?.[missingInteraction.id]) {
|
||||
errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id];
|
||||
}
|
||||
|
||||
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
||||
await this.onStateUpdate<State>(missingInteraction, executionContext, toCache);
|
||||
if (canBeCached(missingInteraction)) {
|
||||
lastConfirmedTxState = {
|
||||
tx: missingInteraction,
|
||||
@@ -267,12 +262,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
state: toCache
|
||||
};
|
||||
}
|
||||
await this.onStateUpdate<State>(
|
||||
missingInteraction,
|
||||
executionContext,
|
||||
toCache,
|
||||
cacheEveryNInteractions % i == 0
|
||||
);
|
||||
}
|
||||
|
||||
if (progressPlugin) {
|
||||
@@ -297,11 +286,17 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
await this.onStateEvaluated(lastConfirmedTxState.tx, executionContext, lastConfirmedTxState.state);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
callingInteraction: this.swGlobal._activeTx,
|
||||
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);
|
||||
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}`
|
||||
: result.errorMessage;
|
||||
|
||||
await executionContext.warp.stateEvaluator.onInternalWriteStateUpdate(this.swGlobal._activeTx, contractTxId, {
|
||||
calleeContract.setUncommittedState(calleeContract.txId(), {
|
||||
state: result.state as State,
|
||||
validity: {
|
||||
...result.originalValidity,
|
||||
@@ -80,6 +80,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
[this.swGlobal._activeTx.id]: effectiveErrorMessage
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldAutoThrow) {
|
||||
throw new ContractError(effectiveErrorMessage);
|
||||
}
|
||||
@@ -117,20 +118,27 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
transaction: this.swGlobal.transaction.id
|
||||
});
|
||||
|
||||
const {contract} = executionContext;
|
||||
const { contract, warp } = executionContext;
|
||||
|
||||
let stateWithValidity;
|
||||
/*let stateWithValidity;
|
||||
if (!contract.isRoot() && contract.hasUncommittedState(contractTxId)) {
|
||||
stateWithValidity = contract.getUncommittedState(contractTxId);
|
||||
} else {
|
||||
const childContract = executionContext.warp.contract(contractTxId, contract, {
|
||||
const childContract = warp.contract(contractTxId, contract, {
|
||||
callingInteraction: interactionTx,
|
||||
callType: 'read'
|
||||
});
|
||||
|
||||
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) {
|
||||
const errorKeys = Reflect.ownKeys(stateWithValidity?.cachedValue?.errorMessages);
|
||||
@@ -160,12 +168,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
|
||||
protected assignRefreshState(executionContext: ExecutionContext<State>) {
|
||||
this.swGlobal.contracts.refreshState = async () => {
|
||||
const stateEvaluator = executionContext.warp.stateEvaluator;
|
||||
const result = await stateEvaluator.latestAvailableState(
|
||||
this.swGlobal.contract.id,
|
||||
this.swGlobal._activeTx.sortKey
|
||||
);
|
||||
return result?.cachedValue.state;
|
||||
return executionContext.contract.getUncommittedState(this.swGlobal.contract.id)?.state;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2161,6 +2161,13 @@ astral-regex@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
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:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
|
||||
|
||||
Reference in New Issue
Block a user