How do you make Atomic Transfer of two different assets?

I tried to make atomic swaps of two different assets but i keep getting this error "transaction Group: [0] has zero Group but was submitted in a group of 2’ ", even though the transaction has a group ID assigned. Any ideas ?

Hi, @xavyer! Is there any source code you could please link or are there demo snippets you could show to help debug?

You have the situation exactly right: for some reason the node thinks the first transaction in the group has no group ID assigned. In my own hacking and demos, I’ve had the problem before where I mistakenly only added the group ID to some transactions in the group, rather than all of them - maybe it’s that? You say it does have a group assigned though :thinking: once I accidentally wrote a helper for signing transactions that also mutated the transactions, clearing some fields, maybe it’s something like that?

I hope this helps!

Hi Evan. Thank you for your response.
I’ve added the source code below.

const algosdk = require(“algosdk”);
const { mnemonic1, mnemonic2, mnemonic3 } = require("…/vars");

const baseServer = “https://testnet-algorand.api.purestake.io/ps1”;
const port = “”;
const token = {
“X-API-Key”: “”
};
const post_txn_token = {
“X-API-Key”: “”,
“content-type”: “application/x-binary”
};

const algodclient = new algosdk.Algod(token, baseServer, port);
const post_algodclient = new algosdk.Algod(post_txn_token, baseServer, port);

const account1 = algosdk.mnemonicToSecretKey(mnemonic1);
const account2 = algosdk.mnemonicToSecretKey(mnemonic2);
const account3 = algosdk.mnemonicToSecretKey(mnemonic3);

// Structure for changing blockchain params
const cp = {
fee: 0,
firstRound: 0,
lastRound: 0,
genID: “”,
genHash: “”
};
//Utility function to update params from blockchain
const getChangingParms = async function(algodclient) {
let params = await algodclient.getTransactionParams();
cp.firstRound = params.lastRound;
cp.lastRound = cp.firstRound + parseInt(1000);
let sfee = await algodclient.suggestedFee();
cp.fee = sfee.fee;
cp.genID = params.genesisID;
cp.genHash = params.genesishashb64;
};
// Function used to wait for a tx confirmation
const waitForConfirmation = async function(algodclient, txId) {
while (true) {
b3 = await algodclient.pendingTransactionInformation(txId);
if (b3.round != null && b3.round > 0) {
//Got the completed Transaction
console.log("Transaction " + b3.tx + " confirmed in round " + b3.round);
break;
}
}
};

// Asset Creation:
async function createAsset(
note = undefined,
creator,
defaultFrozen = false,
totalIssuance = 100,
unitName = “”,
assetName = “”,
assetURL = “http://localhost”,
assetMetadataHash = “174232y32y”,
decimals = 0
) {
await getChangingParms(algodclient);

// create the asset
let addr = (manager = reserve = freeze = clawback = creator.addr);

// signing and sending “txn” allows “addr” to create an asset
let txn = algosdk.makeAssetCreateTxn(
addr,
cp.fee,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
totalIssuance,
decimals,
defaultFrozen,
manager,
reserve,
freeze,
clawback,
unitName,
assetName,
assetURL,
assetMetadataHash
);

let rawSignedTxn = txn.signTxn(creator.sk);
let tx = await post_algodclient.sendRawTransaction(rawSignedTxn);
console.log("Transaction : " + tx.txId);

// wait for transaction to be confirmed and get the assetid
await waitForConfirmation(algodclient, tx.txId);
let ptx = await algodclient.pendingTransactionInformation(tx.txId);
console.log(ptx.txresults.createdasset);
return ptx.txresults.createdasset;
}

async function getAssetInfo(assetID) {
//Get the asset information for the newly changed asset
let assetInfo = await algodclient.assetInformation(assetID);
console.log(“Asset Info”, assetInfo);
return assetInfo;
}

async function getAccountInfo(addr) {
// the new asset listed in the account information
act = await algodclient.accountInformation(addr);
console.log("Account Information for: " + JSON.stringify(act.assets));
}

async function optInForAsset(account, assetID) {
// Opting in to an Asset:
// Transaction from and sender must be the same
const sender = account.addr;
const recipient = sender;
const revocationTarget = undefined;
const closeRemainderTo = undefined;
const note = undefined;
// We are sending 0 of new assets
amount = 0;

// update changing transaction parameters
await getChangingParms(algodclient);

// signing and sending “txn” allows sender to begin accepting asset specified by assetid
const opttxn = algosdk.makeAssetTransferTxn(
sender,
recipient,
closeRemainderTo,
revocationTarget,
cp.fee,
amount,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
assetID
);

// Must be signed by the account wishing to opt in to the asset
const rawSignedTxn = opttxn.signTxn(account.sk);
const opttx = await post_algodclient.sendRawTransaction(rawSignedTxn);
console.log("Transaction : " + opttx.txId);
// wait for transaction to be confirmed
await waitForConfirmation(algodclient, opttx.txId);
}

//submit the transaction
(async () => {
const asset1ID = await createAsset(
undefined,
account1,
false,
100,
“Xs”,
“Xavions”,
http://localhost”,
“16efaa3924a6fd9d3a4824799a4ac65d”,
0
);

const asset2ID = await createAsset(
undefined,
account2,
false,
100,
“Ps”,
“Pracoin”,
http://localhost”,
“16efaa3924a6fd9d3a4824799a4ac65d”,
0
);

const asset1Info = await getAssetInfo(asset1ID);
const asset2Info = await getAssetInfo(asset2ID);

await getAccountInfo(account1.addr);
await getAccountInfo(account2.addr);

await optInForAsset(account2, asset1ID);
await optInForAsset(account1, asset2ID);

const revocationTarget = undefined;
const closeRemainderTo = undefined;
const note = undefined;

// update changing transaction parameters
await getChangingParms(algodclient);

// signing and sending “txn” will send “amount” assets from “sender” to “recipient”
let xtxn = algosdk.makeAssetTransferTxn(
account1.addr,
account2.addr,
closeRemainderTo,
revocationTarget,
cp.fee,
20,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
asset1ID
);

// signing and sending “txn” will send “amount” assets from “sender” to “recipient”
let xtxn1 = algosdk.makeAssetTransferTxn(
account2.addr,
account1.addr,
closeRemainderTo,
revocationTarget,
cp.fee,
20,
cp.firstRound,
cp.lastRound,
note,
cp.genHash,
cp.genID,
asset2ID
);

// Store both transactions
let txns = [xtxn, xtxn1];

// Group both transactions
let txgroup = algosdk.assignGroupID(txns);

// Sign each transaction in the group with
// correct key
let signed =
signed.push( xtxn.signTxn( account1.sk ) )
signed.push( xtxn1.signTxn( account2.sk ) )

let tx = (await post_algodclient.sendRawTransactions(signed));
console.log("Transaction : " + tx.txId);

// Wait for transaction to be confirmed
await waitForConfirmation(algodclient, tx.txId)

await getAccountInfo(account1.addr);
await getAccountInfo(account2.addr);
})().catch(e => {
console.log(e);
console.trace();
});

Hello @xavyer,
I wrote a TEAL program to handle atomic swaps of two ASA. Included in the forum post are instructions and the template. I’d be happy to receive feedback if it addresses your needs.

1 Like

I think the confusion might be here. assignGroupID doesn’t add the groupID in-place, it returns a new array. In other words, xtxn and xtxn1 still have no group ID after the assignGroupID call, instead txgroup will be a copy of txns with the group IDs added. So, I think you want txgroup[0].signTxn(account1.sk) and txgroup[1].signTxn(account1.sk).

@Evan I get same error for this too

@ryanRfox Thank you for your response. I will look into it and let you know.

Can you log or otherwise inspect the txgroup members? I wonder what they look like :thinking:

@Evan Please refer to the video attached below

I got this to work locally on my node by just connecting to it and running your example. It also worked with PureStake connection. Have you updated the algosdk?

@JasonW That was the problem. It works fine after I updated the sdk. Thank you for your time.

1 Like