feat: uncommitted state for internal writes

This commit is contained in:
ppe
2022-12-13 11:08:41 +01:00
committed by just_ppe
parent 51499cadaf
commit 850fca3127
29 changed files with 1570 additions and 106 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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}"`);
}
}
})();

View 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}"`);
}
}
})();

View 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}"`);
}
}
})();

View 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>;

View File

@@ -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)
});
}

View 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
}
))
}

View 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),
}
}

View File

@@ -0,0 +1,5 @@
pub mod evolve;
pub mod balance;
pub mod transfers;
pub mod allowances;

View File

@@ -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))
}

View 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),
}
}

View File

@@ -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!**

View File

@@ -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(&current_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;
}

View File

@@ -0,0 +1,5 @@
/////////////////////////////////////////////////////
/////////////// DO NOT MODIFY THIS FILE /////////////
/////////////////////////////////////////////////////
pub mod entrypoint;

View File

@@ -0,0 +1,10 @@
use serde::Serialize;
#[derive(Serialize)]
pub enum ContractError {
RuntimeError(String),
CallerBalanceNotEnough(u64),
CallerAllowanceNotEnough(u64),
OnlyOwnerCanEvolve,
EvolveNotAllowed
}

View File

@@ -0,0 +1,6 @@
mod state;
mod action;
mod error;
mod actions;
mod contract;
pub mod contract_utils;

View 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>
}

View File

@@ -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 })

View File

@@ -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');
}
});
});

View File

@@ -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,
@@ -61,13 +62,15 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
'mainnet'
)
.useWarpGateway(
{ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null },
{...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null},
{
...defaultCacheOptions,
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,
@@ -103,20 +112,21 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
'mainnet'
)
.useWarpGateway(
{ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null },
{...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null},
{
...defaultCacheOptions,
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);
},

View File

@@ -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>;
}

View File

@@ -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,52 +125,71 @@ 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;
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
this.logger.info('Execution Context', {
srcTxId: executionContext.contractDefinition?.srcTxId,
missingInteractions: executionContext.sortedInteractions?.length,
cachedSortKey: executionContext.cachedState?.sortKey
});
initBenchmark.stop();
if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) {
const result = this.getUncommittedState(this.txId());
return {
sortKey,
cachedValue: result as EvalStateResult<State>
};
}
const stateBenchmark = Benchmark.measure();
const result = await stateEvaluator.eval(executionContext);
stateBenchmark.stop();
// 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 total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number);
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
this.logger.info('Execution Context', {
srcTxId: executionContext.contractDefinition?.srcTxId,
missingInteractions: executionContext.sortedInteractions?.length,
cachedSortKey: executionContext.cachedState?.sortKey
});
initBenchmark.stop();
this._benchmarkStats = {
gatewayCommunication: initBenchmark.elapsed(true) as number,
stateEvaluation: stateBenchmark.elapsed(true) as number,
total
};
const stateBenchmark = Benchmark.measure();
const result = await stateEvaluator.eval(executionContext);
stateBenchmark.stop();
this.logger.info('Benchmark', {
'Gateway communication ': initBenchmark.elapsed(),
'Contract evaluation ': stateBenchmark.elapsed(),
'Total: ': `${total.toFixed(0)}ms`
});
const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number);
return result;
this._benchmarkStats = {
gatewayCommunication: initBenchmark.elapsed(true) as number,
stateEvaluation: stateBenchmark.elapsed(true) as number,
total
};
this.logger.info('Benchmark', {
'Gateway communication ': initBenchmark.elapsed(),
'Contract evaluation ': stateBenchmark.elapsed(),
'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>;
}
}

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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;
};
}
}

View File

@@ -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"