Design Convert System
What is 'Convert'?โ
Have you played ๐ฎBreak The Bricks? (If not, go ahead and try out now!๐พ) In the game, you can earn DIAMOND() through gameplay.
๐คฏDid you know that DIAMOND() is just game data, not a CW20 token for the blockchain?
However, through the 'convert' feature on the Web3 Gaming Ops page, you can exchange your DIAMOND() for ACADEMY-TKN(), the CW20 token.๐ธ๐
Why use the 'Convert' process? Distributing CW20 tokens () directly as game rewards would lead to transaction fees every time. Which could be a pain in the butt for both gamers and game developers at some point. ๐ฌ
To reduce fees, XPLA ACADEMY TEAM suggests designing in-game currency () as rewards, letting users exchange DIAMOND() for CW20 tokens () when they want! In this lesson, we'll look at how the Convert feature can be implemented.
CW20 tokens vs. In-Game Currency
Icon IMG | Name | Type | Storage Location |
---|---|---|---|
DIAMOND | In-game currency | on Game DB as a game data | |
ACADEMY-TKN | CW20 tokens | on XPLA Blockchain Network |
In-game currency is most likely game data, like DIAMOND(). If it's game data, it's unlikely to be saved on the blockchain network.
Learn how Convert works!โ
There are two scenarios when a user requests Convert:
1. I want to change in-game currency() to CW20()
2. I want to change CW20() to in-game currency()
1. Changing in-game currency to CW20โ
Letโs look at the first scenario. The user's amount of in-game currency() is tracked in the game's database, while the ownership of CW20() is recorded on the XPLA blockchain. To convert in-game currency() to CW20(), the game database needs to decrease the user's in-game currency() balance, and CW20() must be sent to the user's wallet.
Normally, when you send tokens on the blockchain, the sender pays the fees. But in Convert, the user getting the tokens pays the fees.
๐ฐIf the game developer pays all Convert fees, it's going to be tough to manage as more users Convert, causing higher expenses. Therefore, a Convert transaction needs signatures from both the game developer's wallet, for sending CW20 tokens, and the user's wallet, for covering the fees. ๐ค๐ (* By "game developer," it could be the game operator, company, etc.)
The first scenario can be represented in a diagram as shown below. It might seem complicated, but taking it step by step makes it easy to grasp.
The example is just one way to set up Convert, missing some details like error handling. There's no one-size-fits-all method; how you design things like API or DB depends on the type of game.
So, use the example code as a guide and adjust it to fit your preferences!
If multiple users send Convert requests simultaneously and the game DEV tries to create several Convert transactions at once, there's a problem. Transactions with the same Sequence value can't be recorded on the blockchain, causing a Sequence Mismatch Error and may disrupt this process. To avoid this, each transaction should have a unique Sequence value, written in the order of the requests.
If you use a Load Balancer for the game server, be cautious to avoid Sequence mismatch errors. If you're unsure about Sequence mismatch, check the Decode Account Number and Sequences in transactions lesson for clarification.
Let's see how it works with the following Pseudo code. (The code won't directly run as is because it's Pseudo code.)
Step 1. User Client App -> Convert Server : Convert request to change in-game currency to CW20import { useWallet } from "@xpla/wallet-provider";
const { wallets } = useWallet();
const userAddress = wallets[0].xplaAddress;
const unsignedResponse = await axios.post(
"https://convertserver.com/gamecurrency-to-coin-unsigned",
{
userAddress: userAddress,
amount : 1,
}
);
User wants to change in-game currency to CW20. The User Client App sends a POST request to the Convert Server, including the user's wallet address (userAddress) and the desired in-game currency amount (amount).
Step 2. Convert Server -> Database (DB) : Check if the user has enough in-game currency and subtract it by the requested amount
db.query('update user_info set game_currency = game_currency - ? where pid = ? ', [req.body.amount, playerId]);
If the user has enough, subtract the requested Convert amount from their balance.
Step 3. Convert Server : Generate an unsigned transaction for the game DEV to send CW20 to user's wallet
import { LCDClient, MsgExecuteContract, TxAPI, Fee } from "@xpla/xpla.js"
const lcd = new LCDClient({ chainID, URL });
const tx_api = new TxAPI(lcd);
const cw20TransferMsg = new MsgExecuteContract(
game_operator_address,
cw20_contract_address,
{
transfer: {
recipient : req.body.userAddress,
amount: String(req.body.amount)
}
}
)
const simul_fee = await tx_api.estimateFee(
[
{
sequenceNumber: game_operator_address_sequence,
publicKey: game_operator_address_pubKey
}
],
{
msgs: [cw20TransferMsg],
gasAdjustment: 1.5,
}
);
const fee = new Fee(simul_fee.gas_limit, simul_fee.amount.toString(), req.body.userAddress); // gas_limit, amount, payer
const tx = await lcd.tx.create([], {msgs: [cw20TransferMsg], fee }) // signers, options
Calculate the estimated transaction fee (simul_fee) and create a transaction for the game DEV's wallet (game_operator_address) to send CW20 to the user's wallet (userAddress).
When setting up the fee variable, make the user's wallet the fee payer by passing it as the payer parameter.
Step 4. Convert Server -> User Client App : Send unsigned transaction to the user
const unsignedTx = Buffer.from(tx.toBytes()).toString('base64')
const result = { unsignedTx : unsignedTx }
return result
Respond to the User Client's POST request by sending the unsigned transaction.
Step 5. User Client App : User signs unsigned transaction. (agreeing to cover fees)
const unsignedResponse = await axios.post(unsignedUrl, unsignedPost);
const unsignedTx = unsignedResponse.data.unsignedTx;
if (unsignedTx === undefined) {
throw new Error("Response is undefined!");
}
const decodedTx = Tx.fromBuffer(Buffer.from(unsignedTx, "base64"));
const { result: signedTx, success } = await connectedWallet.sign({
msgs: decodedTx.body.messages,
fee: decodedTx.auth_info.fee,
signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
});
The user client signs the unsigned transaction (unsignedResponse) from the Convert Server. For the multi-signature Convert transaction in our first scenario, use the SignMode.SIGN_MODE_LEGACY_AMINO_JSON variable.
Step 6. User Client App -> Convert Server : Return the transaction with the user's signature
const userSignedTx = Buffer.from(signedTx.toBytes()).toString("base64");
const res = await axios.post(
"https://convertserver.com/gamecurrency-to-coin-signed",
{
wallet: userAddress,
userSignedTx: userSignedTx,
}
);
The user client sends a POST request to the Convert Server with the transaction info (userSignedTx), including the user wallet address (userAddress) and signature.
Step 7. Convert Server -> Database(DB) : Lock DB, request Sequence Data
const game_opeartor_mk = new MnemonicKey({ mnemonic: opeartorMnemonicKey })
const operatorWallet = lcd.wallet(game_opeartor_mk);
const operatorState = await operatorWallet.accountNumberAndSequence()
const operatorAccNum = operatorState.account_number
await db.beginTransaction()
let txResult;
try {
const [data, ] = await db.query('SELECT sequence FROM operator_sequence WHERE accAddress = ? FOR UPDATE', [operatorWallet.key.accAddress]);
const operatorSeq = data[0].sequence
In the example, beginTransaction
locks the DB, fetching the game operator wallet's Sequence with a query.
Step 8. Convert Server : Sign transaction with the game DEVโs wallet using Sequence Data
try { // step 7
const [data, ] = await db.query('SELECT sequence FROM operator_sequence WHERE accAddress = ? FOR UPDATE', [operatorWallet.key.accAddress]); // step 7
const operatorSeq = data[0].sequence // step 7
const signOption: SignOptions = {
chainID: chainID,
accountNumber: operatorAccNum,
sequence: operatorSeq,
signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON
}
const tx = Tx.fromBuffer(Buffer.from(String(req.body.userSignedTx), 'base64'))
const allSignedTx = await operatorWallet.key.signTx(tx, signOption, false)
allSignedTx.signatures.reverse()
allSignedTx.auth_info.signer_infos.reverse()
...
Add game DEV's wallet signature to the transaction using the received Sequence Data from DB. Sign with SignMode.SIGN_MODE_LEGACY_AMINO_JSON.
Why use reverse() method of signatures and signer_infos in the last part?
We got the user wallet's signature for fees and later received the game DEV wallet's signature for CW20 token transfer. The current order of signatures in the transaction (variable allSignedTx
) is user wallet followed by the game DEV wallet.
Game DEV walletโs signature is needed for the Execute transaction. Userโs signature is added for approval of fee payment. If the user didn't pay the fee, only the game DEV wallet's token transfer signature is needed.
Despite adding a fee payer in Cosmos SDK, the signature order remains the same: game DEV wallet followed by the user wallet. The reverse function in the example adjusts the signature order for successful transaction creation.
Step 9. Convert Server : Execute transaction with both signatures on the XPLA network
txResult = await lcd.tx.broadcastSync(allSignedTx)
Execute transaction on the XPLA network!!
Step 10. Convert Server -> Database(DB) : Send user transaction result
await db.execute(`UPDATE operator_sequence SET sequence = ? WHERE accAddress = ?`, [operatorSeq+1, operatorWallet.key.accAddress])
await db.commit();
db.release();
For a successful transaction, update the game DEV wallet's Sequence Data in the DB and release the lock.
Step 11. Convert Server -> User Client App : Increase the Sequence Data on DB by 1 and unlock it
return { txhash : txResult.txhash }
In response to the User Client's Post request, the transaction propagation result is provided.
2. Changing CW20 to in-game currencyโ
The second scenario is simpler! To convert CW20() to in-game currency(), the user wallet sends CW20() to the game DEV wallet and increases the user's in-game currency() amount in the DB. The process is outlined below.
The user wallet sends CW20() and covers the transaction fee, making Convert transaction creation simple with only the user's signature. No need for the game DEV wallet signature! ใ ก Meaning, no need to worry about Sequence mismatch Errors. Let's see how it works with the following Pseudo code. (The code won't directly run as is because it's Pseudo code.)
Step 1. User Client App -> Convert Server : Request to Convert CW20 to In-Game Currencyimport { useWallet } from "@xpla/wallet-provider";
const { wallets } = useWallet();
const userAddress = wallets[0].xplaAddress;
const unsignedResponse = await axios.post(
"https://convertserver.com/coin-to-gamecurrency-unsigned",
{
userAddress: userAddress,
amount : 1,
}
);
The user wants to exchange CW20 for in-game currency. The user client app sends a Post request to the Convert Server with the user's wallet address and the desired in-game currency amount.
Step 2. Convert Server : Generate an unsigned transaction where the user wallet sends CW20 to the game DEV wallet
import { LCDClient, MsgExecuteContract, TxAPI, Fee } from "@xpla/xpla.js"
const lcd = new LCDClient({ chainID, URL });
const tx_api = new TxAPI(lcd);
const userAccountInfo = await lcd.auth.accountInfo(req.body.userAddress);
const cw20TransferMsg = new MsgExecuteContract(
req.body.userAddress,
cw20_contract_address,
{
transfer: {
recipient : game_operator_address,
amount: String(req.body.amount)
}
}
)
const simul_fee = await tx_api.estimateFee(
[
{
sequenceNumber: userAccountInfo.getSequenceNumber(),
publicKey: userAccountInfo.getPublicKey()
}
],
{
msgs: [cw20TransferMsg],
gasAdjustment: 1.5,
}
);
const fee = new Fee(simul_fee.gas_limit, simul_fee.amount.toString());
const tx = await lcd.tx.create([], {msgs: [cw20TransferMsg], fee } )
Calculate the estimated fee (simul_fee) and create a transaction. The user wallet (userAddress) sends CW20 to the game DEV wallet (game_operator_address) without specifying a payer in the fee variable.
Step 3. Convert Server -> User Client App : Send the unsigned transaction to the user
const unsignedTx = Buffer.from(tx.toBytes()).toString('base64')
const result = { unsignedTx : unsignedTx }
return result
Respond to the User Client's POST request with an unsigned transaction.
Step 4. User Client App : The user signs the unsigned transaction
const unsignedResponse = await axios.post(unsignedUrl, unsignedPost);
const unsignedTx = unsignedResponse.data.unsignedTx;
if (unsignedTx === undefined) {
throw new Error("Response is undefined!");
}
const decodedTx = Tx.fromBuffer(Buffer.from(unsignedTx, "base64"));
const { result: signedTx, success } = await connectedWallet.sign({
msgs: decodedTx.body.messages,
fee: decodedTx.auth_info.fee,
signMode: SignMode.SIGN_MODE_DIRECT,
});
The User Client signs the unsigned transaction (unsignedResponse) for the Convert Server using SignMode.SIGN_MODE_DIRECT. (Since the Convert transaction in second scenario requires single signature)
Step 5. User Client App -> Convert Server : Re-send the transaction with the user's signature
const userSignedTx = Buffer.from(signedTx.toBytes()).toString("base64");
const res = await axios.post(
"https://convertserver.com/coin-to-gamecurrency-signed",
{
wallet: userAddress,
userSignedTx: userSignedTx,
}
);
The User Client sends a POST request to the Convert Server with the user wallet address (userAddress) and the signed transaction (userSignedTx).
Step 6. Convert Server : Execute the transaction with the user's wallet signature to the XPLA network
const tx = Tx.fromBuffer(Buffer.from(String(req.body.userSignedTx), 'base64'))
txResult = await lcd.tx.broadcastSync(allSignedTx)
Execute transaction on the XPLA network!!!
Step 7. Convert Server -> Database(DB) : Increase recorded in-game currency in user's DB as requested
db.query('update user_info set game_currency = game_currency + ? where pid = ? ', [req.body.amount, playerId]);
Increase the requested in-game currency amount (req.body.amount).
Step 8. Convert Server -> User Client App : Send the transaction result to the user
return { txhash : txResult.txhash }
In response to the User Client's Post request, let the user know the transaction result!๐
Wrapping Upโ
How were the details of implementing the Convert feature? With Convert, users can easily exchange in-game currency and CW20. Try out adding a Convert system to your project! ๐