Skip to main content

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 IMGNameTypeStorage Location
DIAMONDIn-game currencyon Game DB as a game data
ACADEMY-TKNCW20 tokenson 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.

User Client AppConvert serverDatabase(DB)Step 3Step 5Step 8Step 9Step 1Step 2Step 4Step 6Step 7Pass Sequence numberStep 10Step 11User Client AppConvert serverDatabase(DB)

danger

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 CW20
User Client App
import { 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
Convert Server
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
Convert Server
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
Convert Server
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)
User Client App
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
User Client App
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
Convert Server
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
Convert Server
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
Convert Server
txResult = await lcd.tx.broadcastSync(allSignedTx)      

Execute transaction on the XPLA network!!


Step 10. Convert Server -> Database(DB) : Send user transaction result
Convert Server
    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
Convert Server
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.

User Client AppConvert serverDatabase(DB)Step 2Step 4Step 6Step 1Step 3Step 5Step 7Step 8User Client AppConvert serverDatabase(DB)

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 Currency
User Client App
import { 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
Convert Server
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
Convert Server
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
User Client App
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
User Client App
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
Convert Server
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
Convert Server
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
Convert Server
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! ๐Ÿ˜‰