Skip to content

Commit

Permalink
fix: nft linker example (#165)
Browse files Browse the repository at this point in the history
* chore: save temp work

* feat: fix and improve nft-linker example

* chore: cleanup example

* chore: update README.md

* chore: add nft-linker to the test

* chore: fix test
  • Loading branch information
npty authored Jan 15, 2024
1 parent a71d251 commit f78578a
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 93 deletions.
4 changes: 2 additions & 2 deletions examples/evm/cross-chain-token/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const name = 'An Awesome Axelar Cross Chain Token';
const symbol = 'AACCT';
const decimals = 13;

async function deploy(chain, wallet) {
async function deploy(chain, wallet, key) {
console.log(`Deploying ERC20CrossChain for ${chain.name}.`);
const provider = getDefaultProvider(chain.rpc);
chain.wallet = wallet.connect(provider);
Expand All @@ -26,7 +26,7 @@ async function deploy(chain, wallet) {
[chain.gateway, chain.gasService, decimals],
[],
defaultAbiCoder.encode(['string', 'string'], [name, symbol]),
'cross-chain-token',
key,
);
console.log(`Deployed ERC20CrossChain for ${chain.name} at ${chain.contract.address}.`);
}
Expand Down
27 changes: 5 additions & 22 deletions examples/evm/nft-linker/NftLinker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//The main function users will interract with.
function sendNFT(
address operator,
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) external payable {
function sendNFT(address operator, uint256 tokenId, string memory destinationChain, address destinationAddress) external payable {
//If we are the operator then this is a minted token that lives remotely.
if (operator == address(this)) {
require(ownerOf(tokenId) == _msgSender(), 'NOT_YOUR_TOKEN');
Expand All @@ -47,12 +42,9 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//Burns and sends a token.
function _sendMintedToken(
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) internal {
function _sendMintedToken(uint256 tokenId, string memory destinationChain, address destinationAddress) internal {
_burn(tokenId);

//Get the original information.
(string memory originalChain, address operator, uint256 originalTokenId) = abi.decode(
original[tokenId],
Expand All @@ -68,12 +60,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//Locks and sends a token.
function _sendNativeToken(
address operator,
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) internal {
function _sendNativeToken(address operator, uint256 tokenId, string memory destinationChain, address destinationAddress) internal {
//Create the payload.
bytes memory payload = abi.encode(chainName, operator, tokenId, destinationAddress);
string memory stringAddress = address(this).toString();
Expand All @@ -84,11 +71,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//This is automatically executed by Axelar Microservices since gas was payed for.
function _execute(
string calldata, /*sourceChain*/
string calldata sourceAddress,
bytes calldata payload
) internal override {
function _execute(string calldata /*sourceChain*/, string calldata sourceAddress, bytes calldata payload) internal override {
//Check that the sender is another token linker.
require(sourceAddress.toAddress() == address(this), 'NOT_A_LINKER');
//Decode the payload.
Expand Down
41 changes: 26 additions & 15 deletions examples/evm/nft-linker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ To deploy the NFT Linker, run the following command:
npm run deploy evm/nft-linker [local|testnet]
```

The aforementioned command pertains to specifying the intended environment for a project to execute on. It provides the option to choose between local and testnet environments by appending either `local` or `testnet` after the command.
The aforementioned command pertains to specifying the intended environment for a project to execute on. It provides the option to choose between local and testnet environments by appending either `local` or `testnet` after the command.

An example of its usage is demonstrated as follows: `npm run deploy evm/nft-linker local` or `npm run deploy evm/nft-linker testnet`.
An example of its usage is demonstrated as follows: `npm run deploy evm/nft-linker local` or `npm run deploy evm/nft-linker testnet`.

A single NFT is minted to the deployer (`0xBa86A5719722B02a5D5e388999C25f3333c7A9fb`) on each chain.

Expand All @@ -35,7 +35,6 @@ npm run execute evm/nft-linker [local|testnet] ${srcChain} ${destChain}

- `srcChain` is `Avalanche`. Valid values are Moonbeam, Avalanche, Fantom, Ethereum, and Polygon
- `destChain` is `Fantom`. Valid values are Moonbeam, Avalanche, Fantom, Ethereum, and Polygon
- `amount` is `10`

**Note**:

Expand All @@ -53,16 +52,28 @@ npm run execute evm/nft-linker local "Avalanche" "Fantom"
Output:

```
--- Initially ---
Token that was originally minted at Moonbeam is at Moonbeam.
Token that was originally minted at Avalanche is at Avalanche.
Token that was originally minted at Fantom is at Fantom.
Token that was originally minted at Ethereum is at Ethereum.
Token that was originally minted at Polygon is at Polygon.
--- Then ---
Token that was originally minted at Moonbeam is at Moonbeam.
Token that was originally minted at Avalanche is at Polygon.
Token that was originally minted at Fantom is at Fantom.
Token that was originally minted at Ethereum is at Ethereum.
Token that was originally minted at Polygon is at Polygon.
==== Initially ====
Minted token ID: '552232130' for Avalanche
Token '552232130' was originally minted at Avalanche is at Avalanche.
==== Approve Original NFT to NFTLinker's contract if needed ====
Approved Original NFT (552232130) to NFTLinker's contract
Tx Approved Hash: 0xb4f199d17785141f933f58e26d62b646461fd18ac878f4c62be10beea7aeab3d
==== Send NFT to NFTLinker's contract ====
Sent NFT 552232130 to NFTLinker's contract 0xa7df30a120c4a99f1843f5df6b5de7cc71fb55046cc25e7bdab5b60def1463ab
Token ID at Fantom will be: '106441082702920043141391782483983459619906865214019059855714686922452481221015'
==== Verify Result ====
Token '106441082702920043141391782483983459619906865214019059855714686922452481221015' was originally minted at Avalanche is at Fantom.
==== Approve Remote NFT to NFTLinker's contract ====
Approved Remote NFT (106441082702920043141391782483983459619906865214019059855714686922452481221015) to NFTLinker's contract
Tx Approved Hash: 0xc2d9c3628ffafea2df0e990bb103a5e2aa5d08ebc5e6f039dec21f17a6cf8699
==== Send NFT back from 'Avalanche' to Fantom ====
Sent NFT 106441082702920043141391782483983459619906865214019059855714686922452481221015 back to Avalanche 0x528bf12f90ac1468dd655624be544ef55c6948bc32bdeb373b7bdec8bec3ff5d
==== Verify Result ====
Token '552232130' was originally minted at Avalanche is at Avalanche.
```
131 changes: 79 additions & 52 deletions examples/evm/nft-linker/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const {
getDefaultProvider,
utils: { keccak256, defaultAbiCoder },
} = require('ethers');
const {
Expand All @@ -13,97 +12,125 @@ const ERC721 = rootRequire('./artifacts/examples/evm/nft-linker/ERC721Demo.sol/E
const ExampleProxy = rootRequire('./artifacts/examples/evm/Proxy.sol/ExampleProxy.json');
const NftLinker = rootRequire('./artifacts/examples/evm/nft-linker/NftLinker.sol/NftLinker.json');

const tokenId = 0;
const tokenId = Math.floor(Math.random() * 1000000000);

async function deploy(chain, wallet) {
console.log(`Deploying ERC721Demo for ${chain.name}.`);
async function deploy(chain, wallet, key) {
chain.erc721 = await deployContract(wallet, ERC721, ['Test', 'TEST']);
console.log(`Deployed ERC721Demo for ${chain.name} at ${chain.erc721.address}.`);
console.log(`Deploying NftLinker for ${chain.name}.`);
const provider = getDefaultProvider(chain.rpc);
chain.wallet = wallet.connect(provider);
console.log(`Deployed ERC721Demo for ${chain.name} at ${chain.erc721.address}`);

chain.contract = await deployUpgradable(
chain.constAddressDeployer,
wallet.connect(provider),
wallet,
NftLinker,
ExampleProxy,
[chain.gateway, chain.gasService],
[],
defaultAbiCoder.encode(['string'], [chain.name]),
key,
);
console.log(`Deployed NftLinker for ${chain.name} at ${chain.contract.address}.`);
console.log(`Minting token ${tokenId} for ${chain.name}`);
await (await chain.erc721.mint(tokenId)).wait();
console.log(`Minted token ${tokenId} for ${chain.name}`);
console.log(`Deployed NftLinker for ${chain.name} at ${chain.contract.address}`);
}

async function execute(chains, wallet, options) {
const { source: originChain, destination, calculateBridgeFee } = options;
const { source, destination, calculateBridgeFee } = options;

const ownerOf = async (chain = originChain) => {
const operator = chain.erc721;
const owner = await operator.ownerOf(tokenId);
const getOwnerDetails = async () => {
const sourceOwner = await source.erc721.ownerOf(tokenId);

if (owner !== chain.contract.address) {
return { chain: chain.name, address: owner, tokenId: BigInt(tokenId) };
if (sourceOwner !== source.contract.address) {
return {
chain: source.name,
ownerAddress: sourceOwner,
tokenId: BigInt(tokenId),
};
}

const newTokenId = BigInt(
keccak256(defaultAbiCoder.encode(['string', 'address', 'uint256'], [chain.name, operator.address, tokenId])),
keccak256(defaultAbiCoder.encode(['string', 'address', 'uint256'], [source.name, source.erc721.address, tokenId])),
);

for (const checkingChain of chains) {
if (checkingChain === chain) continue;
const destOwner = destination.contract.ownerOf(newTokenId);

try {
const address = await checkingChain.contract.ownerOf(newTokenId);
return { chain: checkingChain.name, address, tokenId: newTokenId };
} catch (e) {}
if (destOwner) {
return { chain: destination.name, ownerAddress: destOwner, tokenId: newTokenId };
}

return { chain: '' };
const ownerAddress = await source.erc721.ownerOf(tokenId);
return {
chain: source.name,
ownerAddress,
tokenId: BigInt(tokenId),
};
};

async function print() {
for (const chain of chains) {
const owner = await ownerOf(chain);
console.log(`Token that was originally minted at ${chain.name} is at ${owner.chain}.`);
}
async function print(tokenId) {
const owner = await getOwnerDetails();
console.log(`Token '${tokenId}' was originally minted at ${source.name} is at ${owner.chain}.`);
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const owner = await ownerOf();
const source = chains.find((chain) => chain.name === owner.chain);
if (source === destination) throw new Error('Token is already where it should be!');
console.log('==== Initially ====');
await source.erc721.mint(tokenId);
console.log(`Minted token ID: '${tokenId}' for ${source.name}`);

console.log('--- Initially ---');
await print();
await print(tokenId);

const fee = await calculateBridgeFee(source, destination);

if (originChain === source) {
await (await source.erc721.approve(source.contract.address, owner.tokenId)).wait();
const owner = await getOwnerDetails();
const operatorAddress = source.erc721.address;

// Approve NFT to the NFTLinker contract
console.log(`\n==== Approve Original NFT to NFTLinker's contract if needed ====`);
const srcApproveTx = await source.erc721.approve(source.contract.address, owner.tokenId);
console.log(`Approved Original NFT (${tokenId}) to NFTLinker's contract`);
console.log(`Tx Approved Hash: ${srcApproveTx.hash}`);

// Send NFT to NFTLinker's contract
console.log("\n==== Send NFT to NFTLinker's contract ====");
const sendNftTx = await source.contract.sendNFT(operatorAddress, owner.tokenId, destination.name, wallet.address, { value: fee });
console.log(`Sent NFT ${tokenId} to NFTLinker's contract`, sendNftTx.hash);

const destinationTokenId = BigInt(
keccak256(defaultAbiCoder.encode(['string', 'address', 'uint256'], [source.name, operatorAddress, tokenId])),
);
console.log(`Token ID at ${destination.name} will be: '${destinationTokenId}'`);

while (true) {
const originalDetails = await destination.contract.original(destinationTokenId);
if (originalDetails !== '0x') break;
await sleep(2000);
}

await (
await source.contract.sendNFT(
originChain === source ? source.erc721.address : source.contract.address,
owner.tokenId,
destination.name,
wallet.address,
{ value: fee },
)
).wait();
console.log('\n==== Verify Result ====');
await print(destinationTokenId);

console.log("\n==== Approve Remote NFT to NFTLinker's contract ====");
const destApproveTx = await destination.contract.approve(destination.contract.address, destinationTokenId);
console.log(`Approved Remote NFT (${destinationTokenId}) to NFTLinker's contract`);
console.log(`Tx Approved Hash: ${destApproveTx.hash}`);

console.log(`\n==== Send NFT back from '${source.name}' to ${destination.name} ====`);
const sendBackNftTx = await destination.contract.sendNFT(
destination.contract.address,
destinationTokenId,
source.name,
wallet.address,
{
value: fee,
},
);
console.log(`Sent NFT ${destinationTokenId} back to ${source.name}`, sendBackNftTx.hash);

while (true) {
const owner = await ownerOf();
if (owner.chain === destination.name) break;
const owner = await source.erc721.ownerOf(tokenId);
if (owner === wallet.address) break;
await sleep(2000);
}

console.log('--- Then ---');
await print();
console.log('\n==== Verify Result ====');
await print(tokenId);
}

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions examples/tests/evm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const examples = [
'cross-chain-token',
'deposit-address',
'nonced-execution',
'nft-linker',
'send-ack',
'send-token',
];
Expand Down
3 changes: 2 additions & 1 deletion scripts/libs/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ async function deployOnAltChain(example) {

// Deploy the contracts.
function deployOnEvmChain(chains, wallet, example) {
const key = new Date().getTime().toString();
const deploys = chains.map((chain) => {
const provider = getDefaultProvider(chain.rpc);
return example.deploy(chain, wallet.connect(provider));
return example.deploy(chain, wallet.connect(provider), key);
});

return Promise.all(deploys);
Expand Down
2 changes: 1 addition & 1 deletion scripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ async function calculateBridgeExpressFee(source, destination, options = {}) {
},
);

const expressMultiplier = response.apiResponse.result.express_execute_gas_adjustment_with_multiplier;
const expressMultiplier = response.apiResponse.result.express_execute_gas_multiplier;
const floatToIntFactor = 10000;

// baseFee + executionFeeWithMultiplier + expressFee
Expand Down

0 comments on commit f78578a

Please sign in to comment.