From 685d4664aee9124ad853ef4e9a5e74c7284681a0 Mon Sep 17 00:00:00 2001 From: ACOB-DEV Date: Wed, 25 Mar 2026 04:16:02 +0100 Subject: [PATCH] feat(frontend): add env-based frontend config # Conflicts: # frontend/README.md # frontend/src/Landing.jsx --- frontend/README.md | 28 ++- frontend/src/Landing.css | 39 +++ frontend/src/Landing.jsx | 33 ++- frontend/src/config.js | 34 +++ frontend/src/stellar.js | 523 ++++++++++++++++++--------------------- 5 files changed, 363 insertions(+), 294 deletions(-) create mode 100644 frontend/src/config.js diff --git a/frontend/README.md b/frontend/README.md index 3c65632b..137193a4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -11,13 +11,33 @@ npm run dev Open `http://localhost:5173`. The dev server proxies `/api`, `/api/v1`, and `/health` to the backend on port `3001`. -## Env +## Environment variables -- `VITE_API_URL`: Base URL for API requests. Leave empty to use the local Vite proxy. +Create a `.env.local` file in `frontend/` when you need to point the app at non-default services. + +```bash +VITE_API_URL=http://localhost:3001 +VITE_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org +VITE_REWARDS_CONTRACT_ID=CC... +VITE_CAMPAIGN_CONTRACT_ID=CC... +``` + +- `VITE_API_URL`: Base URL used for frontend `fetch` calls. Leave empty to use the local Vite proxy. +- `VITE_SOROBAN_RPC_URL`: Soroban RPC endpoint used by frontend contract helpers. Defaults to Stellar testnet RPC. +- `VITE_REWARDS_CONTRACT_ID`: Optional rewards contract ID for frontend Soroban calls. +- `VITE_CAMPAIGN_CONTRACT_ID`: Optional campaign contract ID for frontend Soroban calls. ## API routing -The frontend now targets `/api/v1/*` routes by default. Legacy `/api/*` routes are still supported by the backend for backward compatibility, but new integrations should use the v1 prefix. +The frontend targets `/api/v1/*` routes by default. Legacy `/api/*` routes are still supported by the backend for backward compatibility, but new integrations should use the v1 prefix. + +## Config usage + +The frontend reads these values from [src/config.js](/Users/CMI-James/od/Trivela/frontend/src/config.js): + +- API requests are built with `apiUrl(...)`. +- Soroban RPC access goes through `createSorobanServer()`. +- Rewards and campaign contract IDs are exposed through `getRewardsContract()` and `getCampaignContract()`. ## Stellar integration @@ -27,4 +47,4 @@ Use `@stellar/stellar-sdk` for: - Building and signing transactions - Invoking the rewards and campaign contracts -See [Stellar Developers](https://developers.stellar.org/docs) and the root README for contract IDs and flows. +See [Stellar Developers](https://developers.stellar.org/docs) and the root README for deployment flows. diff --git a/frontend/src/Landing.css b/frontend/src/Landing.css index 4a52cc98..956d6116 100644 --- a/frontend/src/Landing.css +++ b/frontend/src/Landing.css @@ -453,6 +453,45 @@ line-height: 1.6; } +.config-grid { + margin-top: 1.5rem; +} + +.config-card { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 16px; + padding: 1.5rem; +} + +.config-card h3 { + font-family: var(--font-heading); + font-size: 1.1rem; + margin: 0 0 0.75rem; +} + +.config-card p { + margin: 0 0 1rem; + color: var(--text-muted); +} + +.config-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.65rem; +} + +.config-list li { + color: var(--text-muted); + word-break: break-word; +} + +.config-list strong { + color: var(--text); +} + /* ---- How it works ---- */ .how { background: var(--bg-elevated); diff --git a/frontend/src/Landing.jsx b/frontend/src/Landing.jsx index 5391d9a2..0ef0b54a 100644 --- a/frontend/src/Landing.jsx +++ b/frontend/src/Landing.jsx @@ -1,4 +1,12 @@ import { useEffect, useState } from 'react'; +import { + apiUrl, + CAMPAIGN_CONTRACT_ID, + REWARDS_CONTRACT_ID, + SOROBAN_RPC_URL, + getCampaignContract, + getRewardsContract, +} from './config'; import { getWalletAddress, fetchRewardsBalance, @@ -6,9 +14,8 @@ import { normalizeError, } from './stellar'; import ClaimRewards from './ClaimRewards'; -import './Landing.css'; - import RegisterCampaign from './RegisterCampaign'; +import './Landing.css'; const GITHUB_REPO = 'https://github.com/FinesseStudioLab/Trivela'; const GITHUB_ISSUES = 'https://github.com/FinesseStudioLab/Trivela/issues'; @@ -22,10 +29,11 @@ export default function Landing() { const [pointsError, setPointsError] = useState(''); const [isWalletLoading, setIsWalletLoading] = useState(false); const [isPointsLoading, setIsPointsLoading] = useState(false); + const rewardsContract = getRewardsContract(); + const campaignContract = getCampaignContract(); useEffect(() => { - const api = import.meta.env.VITE_API_URL || ''; - fetch(`${api}/api/v1/campaigns`) + fetch(apiUrl('/api/v1/campaigns')) .then((r) => r.json()) .then(setCampaigns) .catch(() => setCampaigns([])); @@ -168,7 +176,7 @@ export default function Landing() {
-

What’s in the stack

+

What's in the stack

Soroban contracts, API, and frontend — all open for contribution.

@@ -193,6 +201,21 @@ export default function Landing() {

Vite + Stellar SDK. Landing, campaign list, and hooks for wallet connect and contract calls.

+
+
+

Environment-driven wiring

+

+ Frontend API and Soroban targets are configured through Vite env values so each deployment + can point at its own backend, rewards contract, and campaign contract without code changes. +

+
    +
  • Campaigns API: {apiUrl('/api/v1/campaigns')}
  • +
  • Soroban RPC: {SOROBAN_RPC_URL}
  • +
  • Rewards contract: {rewardsContract ? REWARDS_CONTRACT_ID : 'Not configured'}
  • +
  • Campaign contract: {campaignContract ? CAMPAIGN_CONTRACT_ID : 'Not configured'}
  • +
+
+
diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 00000000..809c92cd --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,34 @@ +import { Contract, Networks, rpc } from '@stellar/stellar-sdk'; + +const DEFAULT_SOROBAN_RPC_URL = 'https://soroban-testnet.stellar.org'; + +function trimTrailingSlash(value) { + return value.replace(/\/+$/, ''); +} + +export const API_BASE_URL = trimTrailingSlash(import.meta.env.VITE_API_URL || ''); +export const SOROBAN_RPC_URL = import.meta.env.VITE_SOROBAN_RPC_URL || DEFAULT_SOROBAN_RPC_URL; +export const REWARDS_CONTRACT_ID = import.meta.env.VITE_REWARDS_CONTRACT_ID || ''; +export const CAMPAIGN_CONTRACT_ID = import.meta.env.VITE_CAMPAIGN_CONTRACT_ID || ''; +export const NETWORK_PASSPHRASE = + import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET; + +export function apiUrl(path) { + if (!path.startsWith('/')) { + throw new Error(`API path must start with "/": ${path}`); + } + + return `${API_BASE_URL}${path}`; +} + +export function createSorobanServer() { + return new rpc.Server(SOROBAN_RPC_URL); +} + +export function getRewardsContract() { + return REWARDS_CONTRACT_ID ? new Contract(REWARDS_CONTRACT_ID) : null; +} + +export function getCampaignContract() { + return CAMPAIGN_CONTRACT_ID ? new Contract(CAMPAIGN_CONTRACT_ID) : null; +} diff --git a/frontend/src/stellar.js b/frontend/src/stellar.js index 22a0b566..5878155b 100644 --- a/frontend/src/stellar.js +++ b/frontend/src/stellar.js @@ -6,135 +6,119 @@ */ import { - Address, - Contract, - Networks, - TransactionBuilder, - BASE_FEE, - scValToNative, - nativeToScVal, - rpc, + Address, + TransactionBuilder, + BASE_FEE, + scValToNative, + nativeToScVal, } from '@stellar/stellar-sdk'; - -/* ---------- environment configuration ---------- */ - -export const SOROBAN_RPC_URL = - import.meta.env.VITE_SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org'; - -export const REWARDS_CONTRACT_ID = import.meta.env.VITE_REWARDS_CONTRACT_ID || ''; - -export const CAMPAIGN_CONTRACT_ID = import.meta.env.VITE_CAMPAIGN_CONTRACT_ID || ''; - -export const NETWORK_PASSPHRASE = - import.meta.env.VITE_STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET; +import { + CAMPAIGN_CONTRACT_ID, + createSorobanServer, + getCampaignContract, + getRewardsContract, + NETWORK_PASSPHRASE, + REWARDS_CONTRACT_ID, +} from './config'; + +export { + CAMPAIGN_CONTRACT_ID, + NETWORK_PASSPHRASE, + REWARDS_CONTRACT_ID, +} from './config'; /* ---------- Freighter helpers ---------- */ -/** - * Return the injected Freighter browser API or throw. - */ export function getFreighterApi() { - const freighterApi = window.freighterApi; + const freighterApi = window.freighterApi; - if (!freighterApi) { - throw new Error( - 'Freighter API is unavailable. Install or unlock the Freighter browser extension.', - ); - } + if (!freighterApi) { + throw new Error( + 'Freighter API is unavailable. Install or unlock the Freighter browser extension.', + ); + } - return freighterApi; + return freighterApi; } -/** - * Connect the Freighter wallet and return the public key. - */ export async function getWalletAddress() { - const freighterApi = getFreighterApi(); + const freighterApi = getFreighterApi(); - const freighterStatus = await freighterApi.isConnected(); - if (freighterStatus.error) throw new Error(freighterStatus.error); - if (!freighterStatus.isConnected) { - throw new Error( - 'Freighter extension was not detected. Install or unlock Freighter to connect a wallet.', - ); - } + const freighterStatus = await freighterApi.isConnected(); + if (freighterStatus.error) throw new Error(freighterStatus.error); + if (!freighterStatus.isConnected) { + throw new Error( + 'Freighter extension was not detected. Install or unlock Freighter to connect a wallet.', + ); + } - const existingAddress = await freighterApi.getAddress(); - if (existingAddress.error) throw new Error(existingAddress.error); - if (existingAddress.address) return existingAddress.address; + const existingAddress = await freighterApi.getAddress(); + if (existingAddress.error) throw new Error(existingAddress.error); + if (existingAddress.address) return existingAddress.address; - const access = await freighterApi.requestAccess(); - if (access.error) throw new Error(access.error); - if (!access.address) throw new Error('Freighter did not return a wallet address.'); + const access = await freighterApi.requestAccess(); + if (access.error) throw new Error(access.error); + if (!access.address) throw new Error('Freighter did not return a wallet address.'); - return access.address; + return access.address; } /* ---------- formatting ---------- */ -/** - * Safely format a raw balance value (bigint | number) to a display string. - */ export function formatPoints(points) { - if (typeof points === 'bigint') return points.toString(); - if (typeof points === 'number') return String(points); - return '0'; + if (typeof points === 'bigint') return points.toString(); + if (typeof points === 'number') return String(points); + return '0'; } -/** - * Turn an unknown error value into a human-readable message. - */ export function normalizeError(error) { - if (!error) return 'Unable to load points right now.'; + if (!error) return 'Unable to load points right now.'; - const message = - typeof error === 'string' - ? error - : error.message || error.toString?.() || 'Unable to load points right now.'; + const message = + typeof error === 'string' + ? error + : error.message || error.toString?.() || 'Unable to load points right now.'; - if (/not found|missing|404/i.test(message)) { - return 'Rewards contract is not deployed on the configured Soroban network yet.'; - } + if (/not found|missing|404/i.test(message)) { + return 'Rewards contract is not deployed on the configured Soroban network yet.'; + } - if (/unsupported address type/i.test(message)) { - return 'Connected wallet address is invalid for Soroban calls.'; - } + if (/unsupported address type/i.test(message)) { + return 'Connected wallet address is invalid for Soroban calls.'; + } - return message; + return message; } /* ---------- contract read helpers ---------- */ -/** - * Simulate a read-only `balance(user)` call and return the raw result. - */ export async function fetchRewardsBalance(walletAddress) { - if (!REWARDS_CONTRACT_ID) { - throw new Error('Set VITE_REWARDS_CONTRACT_ID to load on-chain points.'); - } - - const server = new rpc.Server(SOROBAN_RPC_URL); - const sourceAccount = await server.getAccount(walletAddress); - const contract = new Contract(REWARDS_CONTRACT_ID); - - const transaction = new TransactionBuilder(sourceAccount, { - fee: BASE_FEE, - networkPassphrase: NETWORK_PASSPHRASE, - }) - .addOperation( - contract.call('balance', nativeToScVal(Address.fromString(walletAddress))), - ) - .setTimeout(30) - .build(); - - const simulation = await server.simulateTransaction(transaction); - - if (simulation.error) throw new Error(simulation.error); - if (!simulation.result) { - throw new Error('Soroban RPC returned no result for rewards balance.'); - } - - return scValToNative(simulation.result.retval); + const contract = getRewardsContract(); + if (!contract) { + throw new Error('Set VITE_REWARDS_CONTRACT_ID to load on-chain points.'); + } + + const server = createSorobanServer(); + const sourceAccount = await server.getAccount(walletAddress); + + const transaction = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase: NETWORK_PASSPHRASE, + }) + .addOperation( + contract.call('balance', nativeToScVal(Address.fromString(walletAddress))), + ) + .setTimeout(30) + .build(); + + const simulation = await server.simulateTransaction(transaction); + + if (simulation.error) throw new Error(simulation.error); + if (!simulation.result) { + throw new Error('Soroban RPC returned no result for rewards balance.'); + } + + return scValToNative(simulation.result.retval); } /* ---------- contract write helpers ---------- */ @@ -142,202 +126,171 @@ export async function fetchRewardsBalance(walletAddress) { const TX_POLL_INTERVAL_MS = 1500; const TX_POLL_MAX_ATTEMPTS = 40; -/** - * Build, sign (Freighter), submit, and poll a `claim(user, amount)` call. - * - * Returns `{ hash: string, newBalance: string }` on success. - */ export async function submitClaimTransaction(walletAddress, amount) { - if (!REWARDS_CONTRACT_ID) { - throw new Error('Set VITE_REWARDS_CONTRACT_ID before claiming rewards.'); - } - - const server = new rpc.Server(SOROBAN_RPC_URL); - const sourceAccount = await server.getAccount(walletAddress); - const contract = new Contract(REWARDS_CONTRACT_ID); - - /* 1. Build the transaction */ - const tx = new TransactionBuilder(sourceAccount, { - fee: BASE_FEE, - networkPassphrase: NETWORK_PASSPHRASE, - }) - .addOperation( - contract.call( - 'claim', - nativeToScVal(Address.fromString(walletAddress)), - nativeToScVal(amount, { type: 'u64' }), - ), - ) - .setTimeout(30) - .build(); - - /* 2. Simulate & prepare (assembles auth + resources) */ - const preparedTx = await server.prepareTransaction(tx); - - /* 3. Sign with Freighter */ - const freighterApi = getFreighterApi(); - const signResult = await freighterApi.signTransaction(preparedTx.toXDR(), { - networkPassphrase: NETWORK_PASSPHRASE, - address: walletAddress, - }); - - if (signResult.error) throw new Error(signResult.error); - - /* 4. Re-construct the signed transaction */ - const signedTx = TransactionBuilder.fromXDR( - signResult.signedTxXdr, - NETWORK_PASSPHRASE, - ); - - /* 5. Submit */ - const sendResult = await server.sendTransaction(signedTx); - if (sendResult.status === 'ERROR') { - throw new Error(sendResult.errorResult?.toString() || 'Transaction submission failed.'); - } - - /* 6. Poll until finalised */ - let getResult; - for (let i = 0; i < TX_POLL_MAX_ATTEMPTS; i++) { - // eslint-disable-next-line no-await-in-loop - getResult = await server.getTransaction(sendResult.hash); - if (getResult.status !== 'NOT_FOUND') break; - // eslint-disable-next-line no-await-in-loop - await new Promise((r) => setTimeout(r, TX_POLL_INTERVAL_MS)); - } - - if (!getResult || getResult.status === 'NOT_FOUND') { - throw new Error('Transaction was submitted but could not be confirmed in time.'); - } - - if (getResult.status === 'FAILED') { - throw new Error('Transaction failed on-chain. You may not have enough points.'); - } - - const newBalance = getResult.returnValue - ? formatPoints(scValToNative(getResult.returnValue)) - : null; - - return { hash: sendResult.hash, newBalance }; + const contract = getRewardsContract(); + if (!contract) { + throw new Error('Set VITE_REWARDS_CONTRACT_ID before claiming rewards.'); + } + + const server = createSorobanServer(); + const sourceAccount = await server.getAccount(walletAddress); + + const tx = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase: NETWORK_PASSPHRASE, + }) + .addOperation( + contract.call( + 'claim', + nativeToScVal(Address.fromString(walletAddress)), + nativeToScVal(amount, { type: 'u64' }), + ), + ) + .setTimeout(30) + .build(); + + const preparedTx = await server.prepareTransaction(tx); + + const freighterApi = getFreighterApi(); + const signResult = await freighterApi.signTransaction(preparedTx.toXDR(), { + networkPassphrase: NETWORK_PASSPHRASE, + address: walletAddress, + }); + + if (signResult.error) throw new Error(signResult.error); + + const signedTx = TransactionBuilder.fromXDR( + signResult.signedTxXdr, + NETWORK_PASSPHRASE, + ); + + const sendResult = await server.sendTransaction(signedTx); + if (sendResult.status === 'ERROR') { + throw new Error(sendResult.errorResult?.toString() || 'Transaction submission failed.'); + } + + let getResult; + for (let i = 0; i < TX_POLL_MAX_ATTEMPTS; i++) { + // eslint-disable-next-line no-await-in-loop + getResult = await server.getTransaction(sendResult.hash); + if (getResult.status !== 'NOT_FOUND') break; + // eslint-disable-next-line no-await-in-loop + await new Promise((r) => setTimeout(r, TX_POLL_INTERVAL_MS)); + } + + if (!getResult || getResult.status === 'NOT_FOUND') { + throw new Error('Transaction was submitted but could not be confirmed in time.'); + } + + if (getResult.status === 'FAILED') { + throw new Error('Transaction failed on-chain. You may not have enough points.'); + } + + const newBalance = getResult.returnValue + ? formatPoints(scValToNative(getResult.returnValue)) + : null; + + return { hash: sendResult.hash, newBalance }; } /* ---------- campaign contract helpers ---------- */ -/** - * Simulate a read-only `is_participant(participant)` call. - * - * Returns `true` if the wallet is already registered, `false` otherwise. - */ export async function checkParticipantStatus(walletAddress) { - if (!CAMPAIGN_CONTRACT_ID) { - throw new Error('Set VITE_CAMPAIGN_CONTRACT_ID to check participant status.'); - } - - const server = new rpc.Server(SOROBAN_RPC_URL); - const sourceAccount = await server.getAccount(walletAddress); - const contract = new Contract(CAMPAIGN_CONTRACT_ID); - - const tx = new TransactionBuilder(sourceAccount, { - fee: BASE_FEE, - networkPassphrase: NETWORK_PASSPHRASE, - }) - .addOperation( - contract.call( - 'is_participant', - nativeToScVal(Address.fromString(walletAddress)), - ), - ) - .setTimeout(30) - .build(); - - const simulation = await server.simulateTransaction(tx); - - if (simulation.error) throw new Error(simulation.error); - if (!simulation.result) return false; - - return scValToNative(simulation.result.retval); + const contract = getCampaignContract(); + if (!contract) { + throw new Error('Set VITE_CAMPAIGN_CONTRACT_ID to check participant status.'); + } + + const server = createSorobanServer(); + const sourceAccount = await server.getAccount(walletAddress); + + const tx = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase: NETWORK_PASSPHRASE, + }) + .addOperation( + contract.call( + 'is_participant', + nativeToScVal(Address.fromString(walletAddress)), + ), + ) + .setTimeout(30) + .build(); + + const simulation = await server.simulateTransaction(tx); + + if (simulation.error) throw new Error(simulation.error); + if (!simulation.result) return false; + + return scValToNative(simulation.result.retval); } -/** - * Build, sign (Freighter), submit, and poll a `register(participant)` call - * on the campaign contract. - * - * Returns `{ hash: string, alreadyRegistered: boolean }`. - * - `alreadyRegistered === false` means the user was freshly registered. - * - `alreadyRegistered === true` means they were already registered (contract returned false). - */ export async function submitRegisterTransaction(walletAddress) { - if (!CAMPAIGN_CONTRACT_ID) { - throw new Error('Set VITE_CAMPAIGN_CONTRACT_ID before registering.'); - } - - const server = new rpc.Server(SOROBAN_RPC_URL); - const sourceAccount = await server.getAccount(walletAddress); - const contract = new Contract(CAMPAIGN_CONTRACT_ID); - - /* 1. Build */ - const tx = new TransactionBuilder(sourceAccount, { - fee: BASE_FEE, - networkPassphrase: NETWORK_PASSPHRASE, - }) - .addOperation( - contract.call( - 'register', - nativeToScVal(Address.fromString(walletAddress)), - ), - ) - .setTimeout(30) - .build(); - - /* 2. Simulate & prepare */ - const preparedTx = await server.prepareTransaction(tx); - - /* 3. Sign with Freighter */ - const freighterApi = getFreighterApi(); - const signResult = await freighterApi.signTransaction(preparedTx.toXDR(), { - networkPassphrase: NETWORK_PASSPHRASE, - address: walletAddress, - }); - - if (signResult.error) throw new Error(signResult.error); - - /* 4. Re-construct signed transaction */ - const signedTx = TransactionBuilder.fromXDR( - signResult.signedTxXdr, - NETWORK_PASSPHRASE, + const contract = getCampaignContract(); + if (!contract) { + throw new Error('Set VITE_CAMPAIGN_CONTRACT_ID before registering.'); + } + + const server = createSorobanServer(); + const sourceAccount = await server.getAccount(walletAddress); + + const tx = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase: NETWORK_PASSPHRASE, + }) + .addOperation( + contract.call( + 'register', + nativeToScVal(Address.fromString(walletAddress)), + ), + ) + .setTimeout(30) + .build(); + + const preparedTx = await server.prepareTransaction(tx); + + const freighterApi = getFreighterApi(); + const signResult = await freighterApi.signTransaction(preparedTx.toXDR(), { + networkPassphrase: NETWORK_PASSPHRASE, + address: walletAddress, + }); + + if (signResult.error) throw new Error(signResult.error); + + const signedTx = TransactionBuilder.fromXDR( + signResult.signedTxXdr, + NETWORK_PASSPHRASE, + ); + + const sendResult = await server.sendTransaction(signedTx); + if (sendResult.status === 'ERROR') { + throw new Error( + sendResult.errorResult?.toString() || 'Registration transaction failed.', + ); + } + + let getResult; + for (let i = 0; i < TX_POLL_MAX_ATTEMPTS; i++) { + // eslint-disable-next-line no-await-in-loop + getResult = await server.getTransaction(sendResult.hash); + if (getResult.status !== 'NOT_FOUND') break; + // eslint-disable-next-line no-await-in-loop + await new Promise((r) => setTimeout(r, TX_POLL_INTERVAL_MS)); + } + + if (!getResult || getResult.status === 'NOT_FOUND') { + throw new Error( + 'Registration transaction was submitted but could not be confirmed in time.', ); + } + + if (getResult.status === 'FAILED') { + throw new Error('Registration transaction failed on-chain.'); + } + + const wasNew = getResult.returnValue + ? scValToNative(getResult.returnValue) + : true; - /* 5. Submit */ - const sendResult = await server.sendTransaction(signedTx); - if (sendResult.status === 'ERROR') { - throw new Error( - sendResult.errorResult?.toString() || 'Registration transaction failed.', - ); - } - - /* 6. Poll until finalised */ - let getResult; - for (let i = 0; i < TX_POLL_MAX_ATTEMPTS; i++) { - // eslint-disable-next-line no-await-in-loop - getResult = await server.getTransaction(sendResult.hash); - if (getResult.status !== 'NOT_FOUND') break; - // eslint-disable-next-line no-await-in-loop - await new Promise((r) => setTimeout(r, TX_POLL_INTERVAL_MS)); - } - - if (!getResult || getResult.status === 'NOT_FOUND') { - throw new Error( - 'Registration transaction was submitted but could not be confirmed in time.', - ); - } - - if (getResult.status === 'FAILED') { - throw new Error('Registration transaction failed on-chain.'); - } - - // Contract returns true for a fresh registration, false if already registered. - const wasNew = getResult.returnValue - ? scValToNative(getResult.returnValue) - : true; - - return { hash: sendResult.hash, alreadyRegistered: !wasNew }; + return { hash: sendResult.hash, alreadyRegistered: !wasNew }; }