Skip to main content

State Lookup

In this guide, you will learn how to get the following using the Viem TypeScript library:

  • the Flare address that corresponds to an XRPL account
  • whether a Flare address is a smart account
  • the total value of FXRP that belongs to a personal account tied to an XRPL address
  • a list of all the registered vaults
  • the total value of FXRP of a personal account, in each of the registered vaults
  • a list of all the registered agent vaults

The code example is available on the Flare Foundation GitHub.

src/state-lookup.ts
import { Wallet } from "xrpl";
import {
getAgentVaults,
getOperatorXrplAddresses,
getPersonalAccountAddress,
getVaults,
} from "./utils/smart-accounts";
import { getFxrpBalance } from "./utils/fassets";
import { publicClient } from "./utils/client";
import { erc4626Abi } from "viem";
import type { Address } from "viem";

async function getVaultBalance(vaultAddress: Address, accountAddress: Address) {
const vaultBalance = await publicClient.readContract({
address: vaultAddress,
abi: erc4626Abi,
functionName: "balanceOf",
args: [accountAddress],
});
return vaultBalance;
}

async function main() {
const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);

const operatorXrplAddress = await getOperatorXrplAddresses();
console.log("Operator XRPL addresses:", operatorXrplAddress, "\n");

const personalAccountAddress = await getPersonalAccountAddress(
xrplWallet.address,
);
console.log("Personal account address:", personalAccountAddress, "\n");

const fxrpBalance = await getFxrpBalance(personalAccountAddress);
console.log("Personal account FXRP balance:", fxrpBalance, "\n");

const vaults = await getVaults();
console.log("Vaults:", vaults, "\n");

for (const vault of vaults) {
const vaultBalance = await getVaultBalance(
vault.address,
personalAccountAddress,
);
console.log(`Vault ${vault.id} balance:`, vaultBalance, "\n");
}

const agentVaults = await getAgentVaults();
console.log("Agent vaults:", agentVaults, "\n");
}

void main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Details

Expected output The following is an example of the expected output. The values will differ slightly, but the output should follow the same general format. The personal account address is tied to the XRPL address obtained from the XRPL secret value in the .env file.

Operator XRPL addresses: [ 'rEyj8nsHLdgt79KJWzXR5BgF7ZbaohbXwq' ]

Personal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9F

Personal account FXRP balance: 0n

Vaults: [
{
id: 2n,
address: '0x2C5f203CAd22f3d351912e634dDd0f93C6D503d9',
type: 2
},
{
id: 1n,
address: '0x9BbE85a672Dd2fE2197516656FB2dC76c974954d',
type: 1
}
]

Vault 2 balance: 20000000n

Vault 1 balance: 0n

Agent vaults: [ { id: 1n, address: '0x55c815260cBE6c45Fe5bFe5FF32E3C7D746f14dC' } ]

Let us examine each of the helper functions in more detail. First of all, we need to define a Viem public client. Since the two Flare mainnets and the two testnets are already registered with Viem, we simply need to import them. Then, we create a basic public client with the HTTP transporter.

import { createPublicClient, http } from "viem";
import { flareTestnet } from "viem/chains";

export const publicClient = createPublicClient({
chain: flareTestnet,
transport: http(),
});

With that, we can look at the actual functions. All of them rely primarily on the readContract function of the Viem publicClient to call read functions of contracts on the Flare chain.

Operator's XRPL addresses

The getOperatorXrplAddresses calls the getXrplProviderWallets function of the MasterAccountController contract. The function returns an array of XRPL addresses; these are all the registered operator XRPL addresses.

export async function getOperatorXrplAddresses() {
const result = await publicClient.readContract({
address: await getMasterAccountControllerAddress(),
abi: coston2.iMasterAccountControllerAbi,
functionName: "getXrplProviderWallets",
args: [],
});
return result as string[];
}

We define a helper function for retrieving the address of the MasterAccountController contract from the FlareContractsRegistry.

export async function getMasterAccountControllerAddress(): Promise<Address> {
return getContractAddressByName("MasterAccountController");
}

With the mainnet launch of the Flare smart accounts, the MasterAccountController has been added to the list of official Flare contracts. Its address can be obtained from the FlareContractRegistry contract by calling its getContractAddressByName function.

export async function getContractAddressByName(name: string) {
const contractAddress = await publicClient.readContract({
address: FLARE_CONTRACT_REGISTRY_ADDRESS,
abi: coston2.iFlareContractRegistryAbi,
functionName: "getContractAddressByName",
args: [name],
});

return contractAddress;
}

Checking if a Flare address is a smart account

A Flare address is a smart account (PersonalAccount) if calling the xrplOwner() function on it returns an XRPL account using the IPersonalAccount interface. The PersonalAccount contract exposes the xrplOwner() function — the XRPL address that controls it. If the call succeeds and returns a non-empty and valid XRPL address, the address is a smart account.

export async function getXrplAccountForAddress(
evmAddress: Address,
): Promise<`0x${string}`> {
const xrplOwner = await publicClient.readContract({
address: evmAddress,
abi: coston2.iPersonalAccountAbi,
functionName: "xrplOwner",
args: [],
});
return xrplOwner && xrplOwner.length > 0
? evmAddress
: "0x0000000000000000000000000000000000000000";
}

export async function isSmartAccount(evmAddress: Address): Promise<boolean> {
const smartAccountAddress = await getXrplAccountForAddress(evmAddress);
return smartAccountAddress !== "0x0000000000000000000000000000000000000000";
}

Personal account of an XRPL address

There are many situations when it is important or even necessary to know the address of the PersonalAccount of an XRPL address; like monitoring different emitted events and retrieving user balances. The getPersonalAccountAddress function retrieves the address of the PersonalAccount contract, belonging to the given XRPL address. It calls the getPersonalAccount function of the MasterAccountController contract, with the XRPL address as the function argument.

export async function getPersonalAccountAddress(xrplAddress: string) {
const personalAccountAddress = await publicClient.readContract({
address: await getMasterAccountControllerAddress(),
abi: coston2.iMasterAccountControllerAbi,
functionName: "getPersonalAccount",
args: [xrplAddress],
});

return personalAccountAddress;
}

Account's FXRP balance

The getFxrpBalance function looks up the current FXRP balance of the given address. It calls the balanceOf function, which is a generic ERC-20 function of the FXRP token.

export async function getFxrpBalance(address: Address) {
const fxrpAddress = await getFxrpAddress();
const fxrpBalance = await publicClient.readContract({
address: fxrpAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
});
return fxrpBalance;
}

The FXRP token address is obtained from the AssetManagerFXRP contract by calling its fAsset function.

export async function getFxrpAddress(): Promise<Address> {
const assetManagerAddress = await getAssetManagerFXRPAddress();
const fxrpAddress = await publicClient.readContract({
address: assetManagerAddress,
abi: coston2.iAssetManagerAbi,
functionName: "fAsset",
});
return fxrpAddress;
}

The AssetManagerFXRP contract is one of the contracts registered with the FlareContractRegistry contract. This allows us to read the AssetManagerFXRP contract's address from the latter.

export async function getAssetManagerFXRPAddress(): Promise<Address> {
const assetManagerAddress =
await getContractAddressByName("AssetManagerFXRP");
return assetManagerAddress;
}

Vaults and vault balances

Certain instructions require the user to specify a vault ID. The user should also be able to check their share of each vault to which they have committed their funds. That is why it is important that we know how to retrieve those values.

To get the array of registered vaults, we define two helper types. The getVaults function of the MasterAccountController contract returns an array of three arrays; the first one represents the vault IDs, the second their addresses, and the third their types. The vaults are either of the Firelight (1) or the Upshift (2) type. The GetVaultsReturnType type allows us to properly interpret that data.

The getVaults function calls the function of the MasterAccountController contract with the same name. It iterates over the three sub-arrays and constructs a new array of the Vault type.

export type Vault = {
id: bigint;
address: Address;
type: number;
};

export type GetVaultsReturnType = [bigint[], string[], number[]];

export async function getVaults(): Promise<Vault[]> {
const _vaults = (await publicClient.readContract({
address: await getMasterAccountControllerAddress(),
abi: coston2.iMasterAccountControllerAbi,
functionName: "getVaults",
args: [],
})) as GetVaultsReturnType;

const length = _vaults[0].length;
if (length === 0) {
return [];
}

const vaults = new Array(length) as Vault[];

_vaults[0].forEach((id, index) => {
vaults[index] = {
id,
address: _vaults[1][index]! as Address,
type: _vaults[2][index]!,
};
});

return vaults;
}

On the testnet, the vaults are mocks. They are straightforward ERC-4626 implementations. To get the vault balance of a Flare address, we call the ERC-46426 balanceOf function on the selected vault, with the account address as the argument.

async function getVaultBalance(vaultAddress: Address, accountAddress: Address) {
const vaultBalance = await publicClient.readContract({
address: vaultAddress,
abi: erc4626Abi,
functionName: "balanceOf",
args: [accountAddress],
});
return vaultBalance;
}

Agent vaults

Smart account instructions, which include a minting step (FXRPCollateralReservation, FirelightCollateralReservationAndDeposit, and UpshiftCollateralReservationAndDeposit), require the user to specify the ID of the agent vault they wish to use. We obtain the list of all the registered agent vaults in a similar way to how we do for the deposit vaults.

export type AgentVault = {
id: bigint;
address: Address;
};

export type GetAgentVaultsReturnType = [bigint[], string[]];

export async function getAgentVaults(): Promise<AgentVault[]> {
const _vaults = await publicClient.readContract({
address: await getMasterAccountControllerAddress(),
abi: coston2.iMasterAccountControllerAbi,
functionName: "getAgentVaults",
args: [],
});

const length = _vaults[0].length;
if (length === 0) {
return [];
}

const vaults = new Array(length) as AgentVault[];

_vaults[0].forEach((id, index) => {
vaults[index] = {
id,
address: _vaults[1][index]!,
};
});

return vaults;
}

Video Tutorial