Contexto y escenario de uso
En el envío de transacciones en Solana, el avance de los slots, la asignación de los líderes y la congestión de las rutas de red cambian constantemente qué ruta llega antes al líder. Esto no es propio de ningún proveedor de RPC ni de ningún servicio de envío; es una característica estructural del modelo de ejecución de Solana.
El endpoint SWQoS de ERPC ofrece una ruta de envío que inyecta las transacciones en el carril prioritario que los líderes asignan según Stake-weighted Quality of Service (SWQoS). Este ancho de banda prioritario es aproximadamente 5× el del carril no prioritario y se aplica antes de evaluar las comisiones de prioridad.
Por eso, el endpoint SWQoS es una opción importante para enviar transacciones, pero en producción un único endpoint no siempre es el más rápido. Incluso dentro del mismo slot, diferencias transitorias entre rutas o desequilibrios de carga pueden hacer que otro endpoint rápido llegue primero.
Dadas estas características, en lugar de depender de una sola ruta, puede ser eficaz un patrón operativo que envíe la misma transacción por varias rutas rápidas en paralelo y acepte la primera que se procese.
Sin embargo, cuando se envía la misma transacción a varios endpoints, no se puede garantizar que se ejecute una sola vez sin un control adicional. Un fan-out sin ese control puede provocar una doble ejecución no deseada o romper el control de reintentos.
Solana ofrece Durable Nonce como mecanismo para ello. Con Durable Nonce puedes enviar la misma transacción firmada por varias rutas y, al mismo tiempo, limitar su ejecución on-chain a una sola vez.
Esta página explica cómo implementar operaciones de fan-out que combinan el endpoint SWQoS de ERPC con otros endpoints RPC rápidos, partiendo de que las transacciones se envían con Durable Nonce.
Alcance y requisitos previos
Esta guía explica cómo crear una cuenta de Durable Nonce con web3.js y cómo usarla para enviar transacciones y en operaciones de fan-out.
Requisitos previos que conviene entender:
- En las transacciones con Durable Nonce, usa el valor del nonce como
recentBlockhash y coloca nonceAdvance como primera instrucción
- Una vez que se ejecuta
nonceAdvance, el nonce puede consumirse aunque fallen las instrucciones posteriores. No puedes reenviar el mismo rawTx tal cual.
- La cuenta de nonce se crea una sola vez y normalmente se reutiliza
Paso 1: Preparar la autoridad del nonce y la conexión
La autoridad del nonce es un Keypair que puede avanzar el nonce.
import {
Connection,
Keypair,
SystemProgram,
NONCE_ACCOUNT_LENGTH,
} from '@solana/web3.js'
const connection = new Connection('https://<primary-rpc-endpoint>', 'confirmed')
const nonceAuthority = Keypair.fromSecretKey(/* secret key */)
import {
Connection,
Keypair,
SystemProgram,
NONCE_ACCOUNT_LENGTH,
} from '@solana/web3.js'
const connection = new Connection('https://<primary-rpc-endpoint>', 'confirmed')
const nonceAuthority = Keypair.fromSecretKey(/* secret key */)
- La autoridad del nonce puede ejecutar
nonceAdvance
- Puede ser el fee payer o un keypair distinto
Paso 2: Generar el Keypair de la cuenta de nonce
Una cuenta de nonce es una cuenta del sistema (System Account).
const nonceAccount = Keypair.generate()
const nonceAccount = Keypair.generate()
Este Keypair se usa para:
- Guardar el valor del nonce (sustituye a
recentBlockhash)
- Firmar solo en el momento de la creación
- Después, guardarlo de forma segura; no se necesita para los envíos diarios
Paso 3: Calcular el mínimo para la exención de renta
Las cuentas de nonce deben estar exentas de renta. No escribas los valores a mano en el código; obtenlos del RPC.
const lamports =
await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)
const lamports =
await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)
Paso 4: Crear e inicializar la cuenta de nonce
Crea la cuenta de nonce con createAccount + nonceInitialize en una sola transacción.
import { Transaction } from '@solana/web3.js'
const tx = new Transaction()
tx.add(
SystemProgram.createAccount({
fromPubkey: nonceAuthority.publicKey,
newAccountPubkey: nonceAccount.publicKey,
lamports,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
}),
SystemProgram.nonceInitialize({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
)
// fee payer is the nonce authority
tx.feePayer = nonceAuthority.publicKey
// use a normal blockhash for initialization
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed')
tx.recentBlockhash = blockhash
// sign with both the new account and the authority
tx.sign(nonceAccount, nonceAuthority)
const signature = await connection.sendRawTransaction(tx.serialize())
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
'confirmed',
)
import { Transaction } from '@solana/web3.js'
const tx = new Transaction()
tx.add(
SystemProgram.createAccount({
fromPubkey: nonceAuthority.publicKey,
newAccountPubkey: nonceAccount.publicKey,
lamports,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
}),
SystemProgram.nonceInitialize({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
)
// fee payer is the nonce authority
tx.feePayer = nonceAuthority.publicKey
// use a normal blockhash for initialization
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed')
tx.recentBlockhash = blockhash
// sign with both the new account and the authority
tx.sign(nonceAccount, nonceAuthority)
const signature = await connection.sendRawTransaction(tx.serialize())
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
'confirmed',
)
Usa esta nonceAccount.publicKey en los envíos posteriores.
Paso 5: Obtener el nonce antes de cada envío
En cada transacción, obtén el valor actual del nonce.
import { NonceAccount } from '@solana/web3.js'
const { value, context } = await connection.getAccountInfoAndContext(
nonceAccount.publicKey,
'confirmed',
)
if (!value) {
throw new Error('Nonce account not found')
}
const nonce = NonceAccount.fromAccountData(value.data).nonce
const minContextSlot = context.slot
import { NonceAccount } from '@solana/web3.js'
const { value, context } = await connection.getAccountInfoAndContext(
nonceAccount.publicKey,
'confirmed',
)
if (!value) {
throw new Error('Nonce account not found')
}
const nonce = NonceAccount.fromAccountData(value.data).nonce
const minContextSlot = context.slot
- Usa
nonce como recentBlockhash
context.slot se usa en la confirmación
Paso 6: Construir la transacción con Durable Nonce
Las transacciones con Durable Nonce deben cumplir estas condiciones:
recentBlockhash = nonce
- La primera instrucción es
nonceAdvance
import { TransactionMessage, VersionedTransaction } from '@solana/web3.js'
const instructions = [
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
// add your real instructions after this
]
const message = new TransactionMessage({
payerKey: nonceAuthority.publicKey,
recentBlockhash: nonce,
instructions,
}).compileToV0Message()
const tx = new VersionedTransaction(message)
tx.sign([nonceAuthority /* + other signers */])
const rawTx = tx.serialize()
import { TransactionMessage, VersionedTransaction } from '@solana/web3.js'
const instructions = [
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
// add your real instructions after this
]
const message = new TransactionMessage({
payerKey: nonceAuthority.publicKey,
recentBlockhash: nonce,
instructions,
}).compileToV0Message()
const tx = new VersionedTransaction(message)
tx.sign([nonceAuthority /* + other signers */])
const rawTx = tx.serialize()
Notas:
- Si usas instrucciones de ComputeBudget, deben ir después de
nonceAdvance
- Si
nonceAdvance no es la primera instrucción, la transacción será rechazada
Paso 7: Hacer fan-out a varios RPC
Envía el mismo rawTx de forma concurrente.
const endpoints = [
'https://swqos-fra2.erpc.global?api-key=YOUR_API_KEY',
'https://swqos-ams1.erpc.global?api-key=YOUR_API_KEY',
'https://<backup-rpc-1>',
]
const results = await Promise.allSettled(
endpoints.map((url) =>
new Connection(url, 'confirmed').sendRawTransaction(rawTx, {
skipPreflight: true,
minContextSlot,
}),
),
)
const success = results.find(
(r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled',
)
if (!success) {
throw new Error('All sends failed')
}
const signature = success.value
const endpoints = [
'https://swqos-fra2.erpc.global?api-key=YOUR_API_KEY',
'https://swqos-ams1.erpc.global?api-key=YOUR_API_KEY',
'https://<backup-rpc-1>',
]
const results = await Promise.allSettled(
endpoints.map((url) =>
new Connection(url, 'confirmed').sendRawTransaction(rawTx, {
skipPreflight: true,
minContextSlot,
}),
),
)
const success = results.find(
(r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled',
)
if (!success) {
throw new Error('All sends failed')
}
const signature = success.value
- La firma es idéntica en todos los endpoints
- Un envío correcto no es una confirmación
Paso 8: Confirmar con Durable Nonce
Con Durable Nonce, incluye la información del nonce en la confirmación.
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)
Si la confirmación se completa:
- El nonce avanza
- Las copias enviadas a otros RPC devuelven
InvalidNonce
Envíos siguientes
- Después de la confirmación, obtén un nonce nuevo
- No reutilices el mismo rawTx ni el mismo valor de nonce
- Para flujos de trabajo en paralelo, usa cuentas de nonce distintas
Para la siguiente transacción, obtén el nonce actualizado y construye la transacción con los mismos pasos.