diff --git a/.changeset/gentle-doors-hang.md b/.changeset/gentle-doors-hang.md new file mode 100644 index 00000000..d55a2596 --- /dev/null +++ b/.changeset/gentle-doors-hang.md @@ -0,0 +1,5 @@ +--- +"@folks-finance/algorand-sdk": minor +--- + +updated transactions for new xalgo version diff --git a/.changeset/slimy-drinks-compete.md b/.changeset/slimy-drinks-compete.md new file mode 100644 index 00000000..c79c9e60 --- /dev/null +++ b/.changeset/slimy-drinks-compete.md @@ -0,0 +1,5 @@ +--- +"@folks-finance/algorand-sdk": patch +--- + +stake and deposit util diff --git a/src/xalgo/abi-contracts/index.ts b/src/xalgo/abi-contracts/index.ts index 5fa18b45..462f9831 100644 --- a/src/xalgo/abi-contracts/index.ts +++ b/src/xalgo/abi-contracts/index.ts @@ -1,5 +1,7 @@ import { ABIContract } from "algosdk"; +import stakeAndDepositABI from "./stake_and_deposit.json"; import xAlgoABI from "./xalgo.json"; export const xAlgoABIContract = new ABIContract(xAlgoABI); +export const stakeAndDepositABIContract = new ABIContract(stakeAndDepositABI); diff --git a/src/xalgo/abi-contracts/stake_and_deposit.json b/src/xalgo/abi-contracts/stake_and_deposit.json new file mode 100644 index 00000000..486070cf --- /dev/null +++ b/src/xalgo/abi-contracts/stake_and_deposit.json @@ -0,0 +1,46 @@ +{ + "name": "StakeAndDeposit", + "methods": [ + { + "name": "stake_and_deposit", + "args": [ + { + "type": "pay", + "name": "send_algo" + }, + { + "type": "application", + "name": "consensus" + }, + { + "type": "application", + "name": "pool" + }, + { + "type": "application", + "name": "pool_manager" + }, + { + "type": "asset", + "name": "x_algo" + }, + { + "type": "asset", + "name": "f_x_algo" + }, + { + "type": "account", + "name": "receiver" + }, + { + "type": "uint64", + "name": "min_x_algo_received" + } + ], + "returns": { + "type": "void" + } + } + ], + "networks": {} +} diff --git a/src/xalgo/abi-contracts/xalgo.json b/src/xalgo/abi-contracts/xalgo.json index 6e5b0892..1593d9aa 100644 --- a/src/xalgo/abi-contracts/xalgo.json +++ b/src/xalgo/abi-contracts/xalgo.json @@ -1,20 +1,107 @@ { "name": "XAlgo", + "desc": "Allows users to participate in consensus and receive a liquid staking token", "methods": [ { - "name": "immediate_mint", + "name": "set_proposer_admin", + "desc": "Privileged operation to set the proposer admin.", + "args": [ + { + "type": "uint8", + "name": "proposer_index", + "desc": "The index of proposer to set the admin of" + }, + { + "type": "address", + "name": "new_proposer_admin", + "desc": "The new proposer admin" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "register_online", + "desc": "Privileged operation to register a proposer online", "args": [ { "type": "pay", - "name": "send_algo" + "name": "send_algo", + "desc": "Send ALGO to the proposer to pay for the register online fee" }, { "type": "uint8", - "name": "proposer_index" + "name": "proposer_index", + "desc": "The index of proposer to register online with" + }, + { + "type": "address", + "name": "vote_key", + "desc": "The root participation public key (if any) currently registered for this round" + }, + { + "type": "address", + "name": "sel_key", + "desc": "The selection public key (if any) currently registered for this round" + }, + { + "type": "byte[64]", + "name": "state_proof_key", + "desc": "The root of the state proof key (if any)" }, { "type": "uint64", - "name": "min_received" + "name": "vote_first", + "desc": "The first round for which this participation is valid" + }, + { + "type": "uint64", + "name": "vote_last", + "desc": "The last round for which this participation is valid" + }, + { + "type": "uint64", + "name": "vote_key_dilution", + "desc": "The number of subkeys in each batch of participation keys" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "register_offline", + "desc": "Privileged operation to register a proposer offline", + "args": [ + { + "type": "uint8", + "name": "proposer_index", + "desc": "The index of proposer to register offline with" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "immediate_mint", + "desc": "Send ALGO to the app and receive xALGO immediately", + "args": [ + { + "type": "pay", + "name": "send_algo", + "desc": "Send ALGO to the app to mint" + }, + { + "type": "address", + "name": "receiver", + "desc": "The address to receiver the xALGO at" + }, + { + "type": "uint64", + "name": "min_received", + "desc": "The minimum amount of xALGO to receive in return" } ], "returns": { @@ -23,18 +110,22 @@ }, { "name": "delayed_mint", + "desc": "Send ALGO to the app and receive xALGO after 320 rounds", "args": [ { "type": "pay", - "name": "send_algo" + "name": "send_algo", + "desc": "Send ALGO to the app to mint" }, { - "type": "uint8", - "name": "proposer_index" + "type": "address", + "name": "receiver", + "desc": "The address to receiver the xALGO at" }, { "type": "byte[2]", - "name": "nonce" + "name": "nonce", + "desc": "The nonce used to create the box to store the delayed mint" } ], "returns": { @@ -43,14 +134,17 @@ }, { "name": "claim_delayed_mint", + "desc": "Claim delayed mint after 320 rounds", "args": [ { "type": "address", - "name": "receiver" + "name": "minter", + "desc": "The address which submitted the delayed mint" }, { "type": "byte[2]", - "name": "nonce" + "name": "nonce", + "desc": "The nonce used to create the box which stores the delayed mint" } ], "returns": { @@ -59,18 +153,22 @@ }, { "name": "burn", + "desc": "Send xALGO to the app and receive ALGO", "args": [ { "type": "axfer", - "name": "send_xalgo" + "name": "send_xalgo", + "desc": "Send xALGO to the app to burn" }, { - "type": "uint8", - "name": "proposer_index" + "type": "address", + "name": "receiver", + "desc": "The address to receiver the ALGO at" }, { "type": "uint64", - "name": "min_received" + "name": "min_received", + "desc": "The minimum amount of ALGO to receive in return" } ], "returns": { @@ -79,13 +177,16 @@ }, { "name": "get_xalgo_rate", + "desc": "Get the conversion rate between xALGO and ALGO", "args": [], "returns": { - "type": "(uint64,uint64,byte[])" + "type": "(uint64,uint64,byte[])", + "desc": "Array of [algo_balance, x_algo_circulating_supply, proposers_balances]" } }, { "name": "dummy", + "desc": "Dummy call to the app to bypass foreign accounts limit", "args": [], "returns": { "type": "void" diff --git a/src/xalgo/allocation-strategies/constants.ts b/src/xalgo/allocation-strategies/constants.ts deleted file mode 100644 index e729bf6a..00000000 --- a/src/xalgo/allocation-strategies/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MAX_APPL_CALLS = 5; -export const FIXED_CAPACITY_BUFFER = BigInt(1e6); diff --git a/src/xalgo/allocation-strategies/greedy.ts b/src/xalgo/allocation-strategies/greedy.ts deleted file mode 100644 index 6fe76ff6..00000000 --- a/src/xalgo/allocation-strategies/greedy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { maximum, minimum } from "../../math-lib"; -import { convertAlgoToXAlgoWhenDelay } from "../formulae"; - -import { FIXED_CAPACITY_BUFFER, MAX_APPL_CALLS } from "./constants"; - -import type { ProposerAllocations, ConsensusState } from "../types"; - -const greedyStakeAllocationStrategy = ( - consensusState: ConsensusState, - amount: number | bigint, -): ProposerAllocations => { - const { proposersBalances, maxProposerBalance } = consensusState; - const allocation = new Array(proposersBalances.length).fill(BigInt(0)); - - // sort in ascending order - const indexed = proposersBalances.map((proposer, index) => ({ ...proposer, index })); - indexed.sort((p0, p1) => Number(p0.algoBalance - p1.algoBalance)); - - // allocate to proposers in greedy approach - let remaining = BigInt(amount); - for (let i = 0; i < allocation.length && i < MAX_APPL_CALLS; i++) { - const { algoBalance: proposerAlgoBalance, index: proposerIndex } = indexed[i]; - - // under-approximate capacity to leave wiggle room - const algoCapacity = maximum(maxProposerBalance - proposerAlgoBalance - FIXED_CAPACITY_BUFFER, BigInt(0)); - const allocate = minimum(remaining, algoCapacity); - allocation[proposerIndex] = allocate; - - // exit if fully allocated - remaining -= allocate; - if (remaining <= 0) break; - } - - // handle case where still remaining - if (remaining > 0) throw Error("Insufficient capacity to stake"); - - return allocation; -}; - -const greedyUnstakeAllocationStrategy = ( - consensusState: ConsensusState, - amount: number | bigint, -): ProposerAllocations => { - const { proposersBalances, minProposerBalance } = consensusState; - const allocation = new Array(proposersBalances.length).fill(BigInt(0)); - - // sort in descending order - const indexed = proposersBalances.map((proposer, index) => ({ ...proposer, index })); - indexed.sort((p0, p1) => Number(p1.algoBalance - p0.algoBalance)); - - // allocate to proposers in greedy approach - let remaining = BigInt(amount); - for (let i = 0; i < allocation.length && i < MAX_APPL_CALLS; i++) { - const { algoBalance: proposerAlgoBalance, index: proposerIndex } = indexed[i]; - - // under-approximate capacity to leave wiggle room - const algoCapacity = maximum(proposerAlgoBalance - minProposerBalance - FIXED_CAPACITY_BUFFER, BigInt(0)); - const xAlgoCapacity = convertAlgoToXAlgoWhenDelay(algoCapacity, consensusState); - const allocate = minimum(remaining, xAlgoCapacity); - allocation[proposerIndex] = allocate; - - // exit if fully allocated - remaining -= allocate; - if (remaining <= 0) break; - } - - // handle case where still remaining - if (remaining > 0) throw Error("Insufficient capacity to unstake - override with your own allocation"); - - return allocation; -}; - -export { greedyStakeAllocationStrategy, greedyUnstakeAllocationStrategy }; diff --git a/src/xalgo/allocation-strategies/index.ts b/src/xalgo/allocation-strategies/index.ts deleted file mode 100644 index 5577ea25..00000000 --- a/src/xalgo/allocation-strategies/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./greedy"; diff --git a/src/xalgo/consensus.ts b/src/xalgo/consensus.ts index 2e431c91..696b8391 100644 --- a/src/xalgo/consensus.ts +++ b/src/xalgo/consensus.ts @@ -1,5 +1,3 @@ -import { randomBytes } from "crypto"; - import { AtomicTransactionComposer, decodeAddress, @@ -10,25 +8,22 @@ import { modelsv2, } from "algosdk"; -import { mulScale } from "../math-lib"; import { enc, getApplicationBox, getApplicationGlobalState, getParsedValueFromState, parseUint64s, + PAYOUTS_GO_ONLINE_FEE, signer, transferAlgoOrAsset, } from "../utils"; -import { xAlgoABIContract } from "./abi-contracts"; -import { - greedyStakeAllocationStrategy as defaultStakeAllocationStrategy, - greedyUnstakeAllocationStrategy as defaultUnstakeAllocationStrategy, -} from "./allocation-strategies"; +import { stakeAndDepositABIContract, xAlgoABIContract } from "./abi-contracts"; import type { ConsensusConfig, ConsensusState } from "./types"; -import type { Address, Algodv2, SuggestedParams, Transaction } from "algosdk"; +import type { Pool } from "../lend"; +import type { Algodv2, SuggestedParams, Transaction } from "algosdk"; /** * @@ -39,9 +34,11 @@ import type { Address, Algodv2, SuggestedParams, Transaction } from "algosdk"; * @returns ConsensusState current state of the consensus application */ async function getConsensusState(algodClient: Algodv2, consensusConfig: ConsensusConfig): Promise { + const { consensusAppId } = consensusConfig; + const [{ globalState: state }, { round, value: boxValue }, params] = await Promise.all([ - getApplicationGlobalState(algodClient, consensusConfig.appId), - await getApplicationBox(algodClient, consensusConfig.appId, enc.encode("pr")), + getApplicationGlobalState(algodClient, consensusAppId), + await getApplicationBox(algodClient, consensusAppId, enc.encode("pr")), await algodClient.getTransactionParams().do(), ]); if (state === undefined) throw Error("Could not find xAlgo application"); @@ -51,7 +48,7 @@ async function getConsensusState(algodClient: Algodv2, consensusConfig: Consensu atc.addMethodCall({ sender: "Q5Q5FC5PTYQIUX5PGNTEW22UJHJHVVUEMMWV2LSG6MGT33YQ54ST7FEIGA", signer: makeEmptyTransactionSigner(), - appID: consensusConfig.appId, + appID: consensusAppId, method: getMethodByName(xAlgoABIContract.methods, "get_xalgo_rate"), methodArgs: [], suggestedParams: params, @@ -76,13 +73,11 @@ async function getConsensusState(algodClient: Algodv2, consensusConfig: Consensu // global state const timeDelay = BigInt(getParsedValueFromState(state, "time_delay") || 0); const numProposers = BigInt(getParsedValueFromState(state, "num_proposers") || 0); - const minProposerBalance = BigInt(getParsedValueFromState(state, "min_proposer_balance") || 0); const maxProposerBalance = BigInt(getParsedValueFromState(state, "max_proposer_balance") || 0); const fee = BigInt(getParsedValueFromState(state, "fee") || 0); const premium = BigInt(getParsedValueFromState(state, "premium") || 0); + const lastProposersActiveBalance = BigInt(getParsedValueFromState(state, "last_proposers_active_balance") || 0); const totalPendingStake = BigInt(getParsedValueFromState(state, "total_pending_stake") || 0); - const totalActiveStake = BigInt(getParsedValueFromState(state, "total_active_stake") || 0); - const totalRewards = BigInt(getParsedValueFromState(state, "total_rewards") || 0); const totalUnclaimedFees = BigInt(getParsedValueFromState(state, "total_unclaimed_fees") || 0); const canImmediateStake = Boolean(getParsedValueFromState(state, "can_immediate_mint")); const canDelayStake = Boolean(getParsedValueFromState(state, "can_delay_mint")); @@ -94,13 +89,11 @@ async function getConsensusState(algodClient: Algodv2, consensusConfig: Consensu proposersBalances, timeDelay, numProposers, - minProposerBalance, maxProposerBalance, fee, premium, + lastProposersActiveBalance, totalPendingStake, - totalActiveStake, - totalRewards, totalUnclaimedFees, canImmediateStake, canDelayStake, @@ -116,7 +109,7 @@ function prepareDummyTransaction( atc.addMethodCall({ sender: senderAddr, signer, - appID: consensusConfig.appId, + appID: consensusConfig.consensusAppId, method: getMethodByName(xAlgoABIContract.methods, "dummy"), methodArgs: [], suggestedParams: { ...params, flatFee: true, fee: 1000 }, @@ -128,48 +121,43 @@ function prepareDummyTransaction( return txns[0]; } -// assumes txns has either structure: -// period 1 [appl call, appl call, ...] -// period 2 [transfer, appl call, transfer, appl call, ...] function getTxnsAfterResourceAllocation( consensusConfig: ConsensusConfig, consensusState: ConsensusState, txnsToAllocateTo: Transaction[], - additionalAddresses: Address[], - period: number, + additionalAddresses: string[], senderAddr: string, params: SuggestedParams, ): Transaction[] { - const { appId, xAlgoId } = consensusConfig; + const { consensusAppId, xAlgoId } = consensusConfig; // make copy of txns const txns = txnsToAllocateTo.slice(); - const availableCalls = txns.length / period; + const appCallTxnIndex = txns.length - 1; // add xALGO asset and proposers box - txns[period - 1].appForeignAssets = [xAlgoId]; - const box = { appIndex: appId, name: enc.encode("pr") }; - const { boxes } = txns[period - 1]; + txns[appCallTxnIndex].appForeignAssets = [xAlgoId]; + const box = { appIndex: consensusAppId, name: enc.encode("pr") }; + const { boxes } = txns[appCallTxnIndex]; if (boxes) { boxes.push(box); } else { - txns[period - 1].boxes = [box]; + txns[appCallTxnIndex].boxes = [box]; } // get all accounts we need to add - const accounts: Address[] = additionalAddresses; - for (const { address } of consensusState.proposersBalances) accounts.push(decodeAddress(address)); + const uniqueAddresses: Set = new Set(additionalAddresses); + for (const { address } of consensusState.proposersBalances) uniqueAddresses.add(address); + uniqueAddresses.delete(senderAddr); + const accounts = Array.from(uniqueAddresses).map((address) => decodeAddress(address)); // add accounts in groups of 4 const MAX_FOREIGN_ACCOUNT_PER_TXN = 4; for (let i = 0; i < accounts.length; i += MAX_FOREIGN_ACCOUNT_PER_TXN) { - // which txn to use - const callNum = Math.floor(i / MAX_FOREIGN_ACCOUNT_PER_TXN) + 1; + // which txn to use and check to see if we need to add a dummy call let txnIndex: number; - - // check if we need to add dummy call - if (callNum <= availableCalls) { - txnIndex = callNum * period - 1; + if (Math.floor(i / MAX_FOREIGN_ACCOUNT_PER_TXN) === 0) { + txnIndex = appCallTxnIndex; } else { txns.unshift(prepareDummyTransaction(consensusConfig, senderAddr, params)); txnIndex = 0; @@ -182,6 +170,12 @@ function getTxnsAfterResourceAllocation( return txns; } +function getProposerIndex(consensusState: ConsensusState, proposerAddr: string): number { + const index = consensusState.proposersBalances.findIndex(({ address }) => address === proposerAddr); + if (index === -1) throw Error(`Could not find proposer ${proposerAddr}`); + return index; +} + /** * * Returns a group transaction to stake ALGO and get xALGO immediately. @@ -189,10 +183,10 @@ function getTxnsAfterResourceAllocation( * @param consensusConfig - consensus application and xALGO config * @param consensusState - current state of the consensus application * @param senderAddr - account address for the sender + * @param receiverAddr - account address to receive the xALGO at (typically the user) * @param amount - amount of ALGO to send * @param minReceivedAmount - min amount of xALGO expected to receive * @param params - suggested params for the transactions with the fees overwritten - * @param proposerAllocations - determines which proposers the ALGO sent goes to * @param note - optional note to distinguish who is the minter (must pass to be eligible for revenue share) * @returns Transaction[] stake transactions */ @@ -200,44 +194,119 @@ function prepareImmediateStakeTransactions( consensusConfig: ConsensusConfig, consensusState: ConsensusState, senderAddr: string, + receiverAddr: string, amount: number | bigint, minReceivedAmount: number | bigint, params: SuggestedParams, - proposerAllocations = defaultStakeAllocationStrategy(consensusState, amount), note?: Uint8Array, ): Transaction[] { - const { appId } = consensusConfig; + const { consensusAppId } = consensusConfig; + + const sendAlgo = { + txn: transferAlgoOrAsset(0, senderAddr, getApplicationAddress(consensusAppId), amount, { + ...params, + flatFee: true, + fee: 0, + }), + signer, + }; + const fee = 1000 * (3 + consensusState.proposersBalances.length); const atc = new AtomicTransactionComposer(); - for (const [proposerIndex, splitMintAmount] of proposerAllocations.entries()) { - if (splitMintAmount === BigInt(0)) continue; - - // calculate min received amount by proportional of total mint amount - const splitMinReceivedAmount = mulScale(BigInt(minReceivedAmount), splitMintAmount, BigInt(amount)); - - // generate txns for single proposer - const { address: proposerAddress } = consensusState.proposersBalances[proposerIndex]; - const sendAlgo = { - txn: transferAlgoOrAsset(0, senderAddr, proposerAddress, splitMintAmount, params), - signer, - }; - atc.addMethodCall({ - sender: senderAddr, - signer, - appID: appId, - method: getMethodByName(xAlgoABIContract.methods, "immediate_mint"), - methodArgs: [sendAlgo, proposerIndex, splitMinReceivedAmount], - suggestedParams: { ...params, flatFee: true, fee: 2000 }, - note, - }); - } + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "immediate_mint"), + methodArgs: [sendAlgo, receiverAddr, minReceivedAmount], + suggestedParams: { ...params, flatFee: true, fee }, + note, + }); // allocate resources const txns = atc.buildGroup().map(({ txn }) => { txn.group = undefined; return txn; }); - return getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [], 2, senderAddr, params); + return getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [receiverAddr], senderAddr, params); +} + +/** + * + * Returns a group transaction to stake ALGO and deposit the xALGO received. + * + * @param consensusConfig - consensus application and xALGO config + * @param consensusState - current state of the consensus application + * @param pool - pool application to deposit into + * @param poolManagerAppId - pool manager application + * @param senderAddr - account address for the sender + * @param receiverAddr - account address to receive the deposit (typically the user's deposit escrow or loan escrow) + * @param amount - amount of ALGO to send + * @param minXAlgoReceivedAmount - min amount of xALGO expected to receive + * @param params - suggested params for the transactions with the fees overwritten + * @param note - optional note to distinguish who is the minter (must pass to be eligible for revenue share) + * @returns Transaction[] stake transactions + */ +function prepareImmediateStakeAndDepositTransactions( + consensusConfig: ConsensusConfig, + consensusState: ConsensusState, + pool: Pool, + poolManagerAppId: number, + senderAddr: string, + receiverAddr: string, + amount: number | bigint, + minXAlgoReceivedAmount: number | bigint, + params: SuggestedParams, + note?: Uint8Array, +): Transaction[] { + const { consensusAppId, xAlgoId, stakeAndDepositAppId } = consensusConfig; + const { appId: poolAppId, assetId, fAssetId } = pool; + if (assetId !== xAlgoId) throw Error("xAlgo pool not passed"); + + const sendAlgo = { + txn: transferAlgoOrAsset(0, senderAddr, getApplicationAddress(stakeAndDepositAppId), amount, { + ...params, + flatFee: true, + fee: 0, + }), + signer, + }; + const fee = 1000 * (9 + consensusState.proposersBalances.length); + + const atc = new AtomicTransactionComposer(); + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: stakeAndDepositAppId, + method: getMethodByName(stakeAndDepositABIContract.methods, "stake_and_deposit"), + methodArgs: [ + sendAlgo, + consensusAppId, + poolAppId, + poolManagerAppId, + assetId, + fAssetId, + receiverAddr, + minXAlgoReceivedAmount, + ], + suggestedParams: { ...params, flatFee: true, fee }, + note, + }); + + const txns = atc.buildGroup().map(({ txn }) => { + txn.group = undefined; + return txn; + }); + + // allocate resources, add accounts in groups of 4 + const MAX_FOREIGN_ACCOUNT_PER_TXN = 4; + const accounts = consensusState.proposersBalances.map(({ address }) => decodeAddress(address)); + for (let i = 0; i < accounts.length; i += MAX_FOREIGN_ACCOUNT_PER_TXN) { + txns.unshift(prepareDummyTransaction(consensusConfig, senderAddr, params)); + txns[0].appAccounts = accounts.slice(i, i + 4); + } + txns[0].boxes = [{ appIndex: consensusAppId, name: enc.encode("pr") }]; + return txns; } /** @@ -247,10 +316,11 @@ function prepareImmediateStakeTransactions( * @param consensusConfig - consensus application and xALGO config * @param consensusState - current state of the consensus application * @param senderAddr - account address for the sender + * @param receiverAddr - account address to receive the xALGO at (typically the user) * @param amount - amount of ALGO to send + * @param nonce - used to generate the delayed mint box (must be two bytes in length) * @param params - suggested params for the transactions with the fees overwritten * @param includeBoxMinBalancePayment - whether to include ALGO payment to app for box min balance - * @param proposerAllocations - determines which proposers the ALGO sent goes to * @param note - optional note to distinguish who is the minter (must pass to be eligible for revenue share) * @returns Transaction[] stake transactions */ @@ -258,49 +328,52 @@ function prepareDelayedStakeTransactions( consensusConfig: ConsensusConfig, consensusState: ConsensusState, senderAddr: string, + receiverAddr: string, amount: number | bigint, + nonce: Uint8Array, params: SuggestedParams, includeBoxMinBalancePayment = true, - proposerAllocations = defaultStakeAllocationStrategy(consensusState, amount), note?: Uint8Array, ): Transaction[] { - const { appId } = consensusConfig; + const { consensusAppId } = consensusConfig; + + if (nonce.length !== 2) throw Error(`Nonce must be two bytes`); + // we rely on caller to check nonce is not already in use for sender address + + const sendAlgo = { + txn: transferAlgoOrAsset(0, senderAddr, getApplicationAddress(consensusAppId), amount, { + ...params, + flatFee: true, + fee: 0, + }), + signer, + }; + const fee = 1000 * (2 + consensusState.proposersBalances.length); const atc = new AtomicTransactionComposer(); - for (const [proposerIndex, splitMintAmount] of proposerAllocations.entries()) { - if (splitMintAmount === BigInt(0)) continue; - - // generate txns for single proposer - const { address: proposerAddress } = consensusState.proposersBalances[proposerIndex]; - const sendAlgo = { - txn: transferAlgoOrAsset(0, senderAddr, proposerAddress, splitMintAmount, params), - signer, - }; - const nonce = randomBytes(2); // TODO: safeguard against possible clash? - const boxName = Uint8Array.from([...enc.encode("dm"), ...decodeAddress(senderAddr).publicKey, ...nonce]); - atc.addMethodCall({ - sender: senderAddr, - signer, - appID: appId, - method: getMethodByName(xAlgoABIContract.methods, "delayed_mint"), - methodArgs: [sendAlgo, proposerIndex, nonce], - boxes: [{ appIndex: appId, name: boxName }], - suggestedParams: { ...params, flatFee: true, fee: 2000 }, - note, - }); - } + const boxName = Uint8Array.from([...enc.encode("dm"), ...decodeAddress(senderAddr).publicKey, ...nonce]); + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "delayed_mint"), + methodArgs: [sendAlgo, receiverAddr, nonce], + boxes: [{ appIndex: consensusAppId, name: boxName }], + suggestedParams: { ...params, flatFee: true, fee }, + note, + }); // allocate resources let txns = atc.buildGroup().map(({ txn }) => { txn.group = undefined; return txn; }); - txns = getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [], 2, senderAddr, params); + txns = getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [], senderAddr, params); // add box min balance payment if specified if (includeBoxMinBalancePayment) { const minBalance = BigInt(36100); - txns.unshift(transferAlgoOrAsset(0, senderAddr, getApplicationAddress(appId), minBalance, params)); + txns.unshift(transferAlgoOrAsset(0, senderAddr, getApplicationAddress(consensusAppId), minBalance, params)); } return txns; } @@ -312,7 +385,8 @@ function prepareDelayedStakeTransactions( * @param consensusConfig - consensus application and xALGO config * @param consensusState - current state of the consensus application * @param senderAddr - account address for the sender - * @param receiverAddr - account address for the receiver + * @param minterAddr - account address for the user who submitted the delayed stake + * @param receiverAddr - account address for the receiver of the xALGO * @param nonce - what was used to generate the delayed mint box * @param params - suggested params for the transactions with the fees overwritten * @returns Transaction[] stake transactions @@ -321,21 +395,22 @@ function prepareClaimDelayedStakeTransactions( consensusConfig: ConsensusConfig, consensusState: ConsensusState, senderAddr: string, + minterAddr: string, receiverAddr: string, nonce: Uint8Array, params: SuggestedParams, ): Transaction[] { - const { appId } = consensusConfig; + const { consensusAppId } = consensusConfig; const atc = new AtomicTransactionComposer(); - const boxName = Uint8Array.from([...enc.encode("dm"), ...decodeAddress(receiverAddr).publicKey, ...nonce]); + const boxName = Uint8Array.from([...enc.encode("dm"), ...decodeAddress(minterAddr).publicKey, ...nonce]); atc.addMethodCall({ sender: senderAddr, signer, - appID: appId, + appID: consensusAppId, method: getMethodByName(xAlgoABIContract.methods, "claim_delayed_mint"), - methodArgs: [receiverAddr, nonce], - boxes: [{ appIndex: appId, name: boxName }], + methodArgs: [minterAddr, nonce], + boxes: [{ appIndex: consensusAppId, name: boxName }], suggestedParams: { ...params, flatFee: true, fee: 3000 }, }); @@ -344,15 +419,7 @@ function prepareClaimDelayedStakeTransactions( txn.group = undefined; return txn; }); - return getTxnsAfterResourceAllocation( - consensusConfig, - consensusState, - txns, - [decodeAddress(receiverAddr)], - 1, - senderAddr, - params, - ); + return getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [receiverAddr], senderAddr, params); } /** @@ -362,10 +429,10 @@ function prepareClaimDelayedStakeTransactions( * @param consensusConfig - consensus application and xALGO config * @param consensusState - current state of the consensus application * @param senderAddr - account address for the sender + * @param receiverAddr - account address to receive the xALGO at (typically the user) * @param amount - amount of xALGO to send * @param minReceivedAmount - min amount of ALGO expected to receive * @param params - suggested params for the transactions with the fees overwritten - * @param proposerAllocations - determines which proposers the ALGO received comes from * @param note - optional note to distinguish who is the burner (must pass to be eligible for revenue share) * @returns Transaction[] unstake transactions */ @@ -373,50 +440,216 @@ function prepareUnstakeTransactions( consensusConfig: ConsensusConfig, consensusState: ConsensusState, senderAddr: string, + receiverAddr: string, amount: number | bigint, minReceivedAmount: number | bigint, params: SuggestedParams, - proposerAllocations = defaultUnstakeAllocationStrategy(consensusState, amount), note?: Uint8Array, ): Transaction[] { - const { appId, xAlgoId } = consensusConfig; + const { consensusAppId, xAlgoId } = consensusConfig; + + const sendXAlgo = { + txn: transferAlgoOrAsset(xAlgoId, senderAddr, getApplicationAddress(consensusAppId), amount, { + ...params, + flatFee: true, + fee: 0, + }), + signer, + }; + const fee = 1000 * (3 + consensusState.proposersBalances.length); const atc = new AtomicTransactionComposer(); - for (const [proposerIndex, splitBurnAmount] of proposerAllocations.entries()) { - if (splitBurnAmount === BigInt(0)) continue; - - // calculate min received amount by proportional of total burn amount - const splitMinReceivedAmount = mulScale(BigInt(minReceivedAmount), splitBurnAmount, BigInt(amount)); - - // generate txns for single proposer - const sendXAlgo = { - txn: transferAlgoOrAsset(xAlgoId, senderAddr, getApplicationAddress(appId), splitBurnAmount, params), - signer, - }; - atc.addMethodCall({ - sender: senderAddr, - signer, - appID: appId, - method: getMethodByName(xAlgoABIContract.methods, "burn"), - methodArgs: [sendXAlgo, proposerIndex, splitMinReceivedAmount], - suggestedParams: { ...params, flatFee: true, fee: 2000 }, - note, - }); - } + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "burn"), + methodArgs: [sendXAlgo, receiverAddr, minReceivedAmount], + suggestedParams: { ...params, flatFee: true, fee }, + note, + }); // allocate resources const txns = atc.buildGroup().map(({ txn }) => { txn.group = undefined; return txn; }); - return getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [], 2, senderAddr, params); + return getTxnsAfterResourceAllocation(consensusConfig, consensusState, txns, [receiverAddr], senderAddr, params); +} + +/** + * + * Only for third-party node runners. + * Returns a transaction to set the proposer admin which can register online/offline. + * + * @param consensusConfig - consensus application and xALGO config + * @param consensusState - current state of the consensus application + * @param senderAddr - account address for the sender + * @param proposerAddr - account address of the proposer + * @param newProposerAdminAddr - admin which you want to set + * @param params - suggested params for the transactions with the fees overwritten + * @returns Transaction set proposer admin transaction + */ +function prepareSetProposerAdminTransaction( + consensusConfig: ConsensusConfig, + consensusState: ConsensusState, + senderAddr: string, + proposerAddr: string, + newProposerAdminAddr: string, + params: SuggestedParams, +): Transaction { + const { consensusAppId } = consensusConfig; + const proposerIndex = getProposerIndex(consensusState, proposerAddr); + + const atc = new AtomicTransactionComposer(); + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "set_proposer_admin"), + methodArgs: [proposerIndex, newProposerAdminAddr], + boxes: [ + { appIndex: consensusAppId, name: enc.encode("pr") }, + { + appIndex: consensusAppId, + name: Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposerAddr).publicKey]), + }, + ], + suggestedParams: { ...params, flatFee: true, fee: 1000 }, + }); + const txns = atc.buildGroup().map(({ txn }) => { + txn.group = undefined; + return txn; + }); + return txns[0]; +} + +/** + * + * Only for third-party node runners. + * Returns a transaction to register a proposer online. + * + * @param consensusConfig - consensus application and xALGO config + * @param consensusState - current state of the consensus application + * @param senderAddr - account address for the sender + * @param proposerAddr - account address of the proposer + * @param voteKey - vote key + * @param selectionKey - selection key + * @param stateProofKey - state proof key + * @param voteFirstRound - vote first round + * @param voteLastRound - vote last round + * @param voteKeyDilution - vote key dilution + * @param params - suggested params for the transactions with the fees overwritten + * @returns Transaction register online transaction + */ +function prepareRegisterProposerOnlineTransactions( + consensusConfig: ConsensusConfig, + consensusState: ConsensusState, + senderAddr: string, + proposerAddr: string, + voteKey: Buffer, + selectionKey: Buffer, + stateProofKey: Buffer, + voteFirstRound: number | bigint, + voteLastRound: number | bigint, + voteKeyDilution: number | bigint, + params: SuggestedParams, +): Transaction[] { + const { consensusAppId } = consensusConfig; + const proposerIndex = getProposerIndex(consensusState, proposerAddr); + + const sendAlgo = { + txn: transferAlgoOrAsset(0, senderAddr, proposerAddr, PAYOUTS_GO_ONLINE_FEE, { ...params, flatFee: true, fee: 0 }), + signer, + }; + + const atc = new AtomicTransactionComposer(); + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "register_online"), + methodArgs: [ + sendAlgo, + proposerIndex, + encodeAddress(voteKey), + encodeAddress(selectionKey), + stateProofKey, + voteFirstRound, + voteLastRound, + voteKeyDilution, + ], + appAccounts: [proposerAddr], + boxes: [ + { appIndex: consensusAppId, name: enc.encode("pr") }, + { + appIndex: consensusAppId, + name: Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposerAddr).publicKey]), + }, + ], + suggestedParams: { ...params, flatFee: true, fee: 3000 }, + }); + return atc.buildGroup().map(({ txn }) => { + txn.group = undefined; + return txn; + }); +} + +/** + * + * Only for third-party node runners. + * Returns a transaction to register a proposer offline. + * + * @param consensusConfig - consensus application and xALGO config + * @param consensusState - current state of the consensus application + * @param senderAddr - account address for the sender + * @param proposerAddr - account address of the proposer + * @param params - suggested params for the transactions with the fees overwritten + * @returns Transaction register offline transaction + */ +function prepareRegisterProposerOfflineTransaction( + consensusConfig: ConsensusConfig, + consensusState: ConsensusState, + senderAddr: string, + proposerAddr: string, + params: SuggestedParams, +): Transaction { + const { consensusAppId } = consensusConfig; + const proposerIndex = getProposerIndex(consensusState, proposerAddr); + + const atc = new AtomicTransactionComposer(); + atc.addMethodCall({ + sender: senderAddr, + signer, + appID: consensusAppId, + method: getMethodByName(xAlgoABIContract.methods, "register_offline"), + methodArgs: [proposerIndex], + appAccounts: [proposerAddr], + boxes: [ + { appIndex: consensusAppId, name: enc.encode("pr") }, + { + appIndex: consensusAppId, + name: Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposerAddr).publicKey]), + }, + ], + suggestedParams: { ...params, flatFee: true, fee: 2000 }, + }); + const txns = atc.buildGroup().map(({ txn }) => { + txn.group = undefined; + return txn; + }); + return txns[0]; } export { getConsensusState, prepareDummyTransaction, prepareImmediateStakeTransactions, + prepareImmediateStakeAndDepositTransactions, prepareDelayedStakeTransactions, prepareClaimDelayedStakeTransactions, prepareUnstakeTransactions, + prepareSetProposerAdminTransaction, + prepareRegisterProposerOnlineTransactions, + prepareRegisterProposerOfflineTransaction, }; diff --git a/src/xalgo/constants/mainnet-constants.ts b/src/xalgo/constants/mainnet-constants.ts index 1c5336c2..946c14d6 100644 --- a/src/xalgo/constants/mainnet-constants.ts +++ b/src/xalgo/constants/mainnet-constants.ts @@ -1,8 +1,9 @@ import type { ConsensusConfig } from "../types"; -const consensusConfig: ConsensusConfig = { - appId: 1134695678, +const MainnetConsensusConfig: ConsensusConfig = { + consensusAppId: 1134695678, xAlgoId: 1134696561, + stakeAndDepositAppId: 2633147490, }; -export { consensusConfig }; +export { MainnetConsensusConfig }; diff --git a/src/xalgo/constants/testnet-constants.ts b/src/xalgo/constants/testnet-constants.ts new file mode 100644 index 00000000..670382bf --- /dev/null +++ b/src/xalgo/constants/testnet-constants.ts @@ -0,0 +1,9 @@ +import type { ConsensusConfig } from "../types"; + +const TestnetConsensusConfig: ConsensusConfig = { + consensusAppId: 730430673, + xAlgoId: 730430700, + stakeAndDepositAppId: 731190793, +}; + +export { TestnetConsensusConfig }; diff --git a/src/xalgo/index.ts b/src/xalgo/index.ts index 45b7a257..55c96d1e 100644 --- a/src/xalgo/index.ts +++ b/src/xalgo/index.ts @@ -1,6 +1,6 @@ export * from "./abi-contracts"; -export * from "./allocation-strategies"; export * from "./constants/mainnet-constants"; +export * from "./constants/testnet-constants"; export * from "./consensus"; export * from "./formulae"; export * from "./types"; diff --git a/src/xalgo/types.ts b/src/xalgo/types.ts index 120dccd7..4b737e3f 100644 --- a/src/xalgo/types.ts +++ b/src/xalgo/types.ts @@ -1,6 +1,7 @@ interface ConsensusConfig { - appId: number; + consensusAppId: number; xAlgoId: number; + stakeAndDepositAppId: number; } interface ConsensusState { @@ -13,13 +14,11 @@ interface ConsensusState { }[]; timeDelay: bigint; numProposers: bigint; - minProposerBalance: bigint; maxProposerBalance: bigint; fee: bigint; // 4 d.p. premium: bigint; // 16 d.p. + lastProposersActiveBalance: bigint; totalPendingStake: bigint; - totalActiveStake: bigint; - totalRewards: bigint; totalUnclaimedFees: bigint; canImmediateStake: boolean; canDelayStake: boolean;