Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure and add nft-chain-keys #29

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 16 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
# NEAR Multichain Examples w/ Chain Signatures

![alpha badge](https://img.shields.io/badge/status-alpha-red)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-green)](https://github.com/near-examples/near-multichain/pulls)
[![Chain Signatures Docs](https://img.shields.io/badge/Chain_Signatures_Docs-blue)](https://near-docs.onrender.com/concepts/abstraction/chain-signatures)
[![Dev Support](https://img.shields.io/badge/DEV_SUPPORT-red)](https://t.me/neardev)

An example on signing and executing transactions across multiple blockchain protocols from one NEAR account

> Currently only available on **testnet**, and should be used to control only `testnet` accounts on other chains

---

## Requirements

- `npm` or `yarn`
- NEAR `testnet` account using [MyNEARWallet](https://mynearwallet.com/)

## Installation

```bash
npm install # or yarn
npm run dev # or yarn dev
```

> [!CAUTION]
> Chain Signatures are currently in `alpha` and should only be used in a `testnet` environment.
# NEAR Multichain Examples w/ Chain Signatures

![alpha badge](https://img.shields.io/badge/status-alpha-red)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-green)](https://github.com/near-examples/near-multichain/pulls)
[![Chain Signatures Docs](https://img.shields.io/badge/Chain_Signatures_Docs-blue)](https://docs.near.org/concepts/abstraction/chain-signatures)
[![Dev Support](https://img.shields.io/badge/DEV_SUPPORT-red)](https://t.me/neardev)

This repo contains multiple examples of signing and executing transactions across multiple blockchain protocols from a NEAR account

- [standard-multichain](./standard-multichain/): send transactions to foreign chains from a NEAR account.
- [nft-chain-keys](./nft-chain-keys/): send transactions to Ethereum through transferable NFT Chain Keys.

> Currently only available on **testnet**, and should be used to control only `testnet` accounts on other chains

> [!CAUTION]
> Chain Signatures are currently in `alpha` and should only be used in a `testnet` environment.
File renamed without changes.
26 changes: 26 additions & 0 deletions nft-chain-keys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# nft-chain-keys

An example of signing and executing transactions for Ethereum from a NEAR account through NFT Chain Keys. This enables Ethereum accounts to be swapped via NFT standards on NEAR.

> Currently only available on **testnet**, and should be used to control only `testnet` accounts on other chains

---

## Requirements

- `npm` or `yarn`
- NEAR `testnet` account using [MyNEARWallet](https://testnet.mynearwallet.com/) or [Meteor Wallet](https://wallet.meteorwallet.app/wallet)

## Installation

```bash
npm install # or yarn
npm run dev # or yarn dev
```

---

For a detailed explanation of how this example works, please refer to the [documentation](https://docs.near.org/build/chain-abstraction/nft-chain-keys).

> [!CAUTION]
> Chain Signatures are currently in `alpha` and should only be used in a `testnet` environment.
File renamed without changes.
49 changes: 49 additions & 0 deletions nft-chain-keys/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "new",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ethereumjs/common": "^4.3.0",
"@ethereumjs/tx": "^5.3.0",
"@ethereumjs/util": "^9.0.3",
"@near-wallet-selector/core": "^8.9.5",
"@near-wallet-selector/meteor-wallet": "^8.9.5",
"@near-wallet-selector/modal-ui": "^8.9.5",
"@near-wallet-selector/my-near-wallet": "^8.9.5",
"axios": "^1.6.8",
"bitcoinjs-lib": "^6.1.5",
"bn.js": "^5.2.1",
"bs58check": "^3.0.1",
"elliptic": "^6.5.5",
"ethers": "^6.11.1",
"hash.js": "^1.1.7",
"keccak": "^3.0.4",
"near-api-js": "^3.0.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "^7.8.1",
"vite-plugin-node-polyfills": "^0.21.0",
"web3": "^4.6.0"
},
"overrides": {
"near-api-js": "^3.0.4"
},
"devDependencies": {
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"vite": "^5.1.6"
}
}
File renamed without changes
75 changes: 75 additions & 0 deletions nft-chain-keys/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { NearContext } from './context';

import { useEffect, useState } from "react";
import Navbar from "./components/Navbar"
import { Wallet } from "./services/near-wallet";
import { NFTView } from "./components/NFT";
import { EthereumView } from "./components/Ethereum";

// CONSTANTS
const NFT_CONTRACT = 'v2.nft.kagi.testnet';

// NEAR WALLET
const wallet = new Wallet({ network: 'testnet', createAccessKeyFor: NFT_CONTRACT });

function App() {
const [signedAccountId, setSignedAccountId] = useState('');
const [status, setStatus] = useState("Please login to request a signature");
const [tokenId, setTokenId] = useState('');
const [transactionHash, setTransactionHash] = useState('');

useEffect(() => {
wallet.startUp(setSignedAccountId)
}, []);

useEffect(() => {
if (signedAccountId && tokenId == '') {
setStatus("Please select a NFT");
} else if (! signedAccountId) {
setStatus("Please login to request a signature");
}
}, [signedAccountId]);


useEffect(() => {
// Get transaction hash from URL when using web wallet
const getTransactionHashFromUrl = () => {
const urlParams = new URLSearchParams(window.location.search);
const hash = urlParams.get('transactionHashes');
setTransactionHash(hash);
};

getTransactionHashFromUrl();
}, []);

return (
<NearContext.Provider value={{ wallet, signedAccountId, tokenId, setTokenId }}>
<Navbar />
<div className="container">
<h4> 🔗 Ethereum NFT accounts </h4>
<p className="small">
Send Ethereum transactions through NFT Chain Keys. Learn more in the <a href="https://docs.near.org/build/chain-abstraction/nft-chain-keys"> <b>documentation</b></a>.
</p>

{signedAccountId &&
<div style={{ width: '50%', minWidth: '400px' }}>

<div className="input-group input-group-sm mt-3 mb-4">
<input className="form-control text-center" type="text" value={`NFT Chain Keys Contract: ${NFT_CONTRACT}`} disabled />
</div>

<NFTView props={{ NFT_CONTRACT }} />

<EthereumView props={{ setStatus, NFT_CONTRACT, transactionHash }} />
</div>
}

<div className="mt-3 small text-center">
{status}
</div>
</div>
</NearContext.Provider>
)
}

export default App
160 changes: 160 additions & 0 deletions nft-chain-keys/src/components/Ethereum.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useState, useEffect, useContext } from "react";
import { NearContext } from "../context";

import { Ethereum } from "../services/ethereum";
import { useDebounce } from "../hooks/debounce";
import PropTypes from 'prop-types';

const Sepolia = 11155111;
const Eth = new Ethereum('https://rpc2.sepolia.org', Sepolia);

export function EthereumView({ props: { setStatus, NFT_CONTRACT, transactionHash } }) {
const { wallet, signedAccountId, tokenId, setTokenId } = useContext(NearContext);

const [receiver, setReceiver] = useState("0xe0f3B7e68151E9306727104973752A415c2bcbEb");
const [amount, setAmount] = useState(0.01);
const [loading, setLoading] = useState(false);
const [step, setStep] = useState("request");
const [signedTransaction, setSignedTransaction] = useState(null);
const [senderAddress, setSenderAddress] = useState('');

const [derivation, setDerivation] = useState("ethereum-1");
const derivation_path = useDebounce(derivation, 1000);

useEffect(() => {
// Only called if using web wallet
if (transactionHash != null && sessionStorage.getItem('chain') == "ETH") {
resetParams();
handleCallback();
}
}, [transactionHash]);

// Reset params to before asking for signature if using web wallet
async function resetParams() {
try {
const args = await wallet.getTransactionArgs(transactionHash);
setTokenId(args.token_id);
setDerivation(args.path);
setAmount(sessionStorage.getItem('amount'));
} catch (e) {
setStatus(`❌ Error: ${e.message}`);
}
}

// Handles the rest of the signature method if using web wallet
async function handleCallback() {
try {
setLoading(true);
const signedTransaction = await Eth.requestSignatureFromNFTCallback(wallet, transactionHash);
setSignedTransaction(signedTransaction);
setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
setStep('relay');
setLoading(false);
} catch (e) {
setStatus(`❌ Error: ${e.message}`);
setLoading(false);
}
}

useEffect(() => {
setSenderAddress('Waiting for you to stop typing...')
}, [derivation]);

useEffect(() => {
if (tokenId == '') {
setSenderAddress('Select NFT');
} else {
setEthAddress();
}

async function setEthAddress() {
setStatus('Querying your address and balance');
setSenderAddress(`Deriving address from path ${derivation_path}...`);

const { address } = await Eth.deriveAddress(NFT_CONTRACT, derivation_path, tokenId);
setSenderAddress(address);

const balance = await Eth.getBalance(address);
setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
}
}, [signedAccountId, derivation_path, tokenId]);

async function chainSignature() {
setStatus('🏗️ Creating transaction');
const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount);

setStatus(`🕒 Asking ${NFT_CONTRACT} to sign the transaction, this might take a while`);
try {
const signedTransaction = await Eth.requestSignatureFromNFT(wallet, tokenId, NFT_CONTRACT, derivation_path, payload, transaction, senderAddress);
setSignedTransaction(signedTransaction);
setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
setStep('relay');
} catch (e) {
setStatus(`❌ Error: ${e.message}`);
setLoading(false);
}
}

async function relayTransaction() {
setLoading(true);
setStatus('🔗 Relaying transaction to the Ethereum network... this might take a while');

try {
const txHash = await Eth.relayTransaction(signedTransaction);
setStatus(<>
<a href={`https://sepolia.etherscan.io/tx/${txHash}`} target="_blank"> ✅ Successful </a>
</>
);
} catch (e) {
setStatus(`❌ Error: ${e.message}`);
}

setStep('request');
setLoading(false);
window.history.pushState({}, '', window.location.origin);
}

const UIChainSignature = async () => {
setLoading(true);
await chainSignature();
setLoading(false);
}

return (
<>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">Path:</label>
<div className="col-sm-10">
<input type="text" className="form-control form-control-sm" value={derivation} onChange={(e) => setDerivation(e.target.value)} disabled={loading} />
<div className="form-text" id="eth-sender"> {senderAddress} </div>
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">To:</label>
<div className="col-sm-10">
<input type="text" className="form-control form-control-sm" value={receiver} onChange={(e) => setReceiver(e.target.value)} disabled={loading} />
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">Amount:</label>
<div className="col-sm-10">
<input type="number" className="form-control form-control-sm" value={amount} onChange={(e) => setAmount(e.target.value)} step="0.01" disabled={loading} />
<div className="form-text"> Ethereum units </div>
</div>
</div>

<div className="text-center">
{step === 'request' && <button className="btn btn-primary text-center" onClick={UIChainSignature} disabled={loading}> Request Signature </button>}
{step === 'relay' && <button className="btn btn-success text-center" onClick={relayTransaction} disabled={loading}> Relay Transaction </button>}
</div>

</>
)
}

EthereumView.propTypes = {
props: PropTypes.shape({
setStatus: PropTypes.func.isRequired,
NFT_CONTRACT: PropTypes.string.isRequired,
}).isRequired
};
Loading