Гайд: Спонсируемые комиссии и резервы (XLS-68)

Как использовать XRPL Sponsored Fees & Reserves через XrplCSharp SDK. Спонсорство позволяет одному аккаунту (спонсору) платить комиссии транзакций и/или резервы объектов за другой аккаунт (спонсируемого) — базовый механизм для «безгазового» онбординга и кастодиальных сценариев.

Важно: требуется амендмент Sponsor (XLS-68). На середину 2026 он существует только в ветке rippled develop — не входит ни в один релиз и не активен ни в mainnet, ни в testnet. Для экспериментов используйте nightly-стенд из этого репозитория (см. Тестирование). Фича в статусе draft и может меняться.

Содержание

  • Обзор
  • Ключевые понятия
  • Типы транзакций
  • Пошагово: создание спонсорства
  • Отправка спонсируемой транзакции
  • Сценарии подписания (V1/V2/V3)
  • Спонсорство внутри Batch
  • Комиссии
  • Объекты леджера
  • Тестирование
  • Типичные ошибки

Обзор

Спонсор (rSponsor...)                   Спонсируемый (rUser...)
┌───────────────────────┐               ┌────────────────────────┐
│ SponsorshipSet        │──────────────►│ может слать транзакции │
│   Sponsee: rUser...   │               │   Sponsor: rSponsor... │
│   FeeAmount: 5 XRP    │               │   SponsorFlags: fee    │
│   RemainingOwnerCount │               │ комиссию платит спонсор│
└───────────────────────┘               └────────────────────────┘
        создаёт
┌───────────────────────┐
│ Sponsorship (леджер)  │
│   Owner / Sponsee     │
│   бюджет FeeAmount    │
│   RemainingOwnerCount │
└───────────────────────┘

Спонсируются два независимых измерения:

Измерение Значение SponsorFlags Что платит спонсор
Комиссии SponsorCoverage.spfSponsorFee (= 1) Fee транзакций спонсируемого
Резервы SponsorCoverage.spfSponsorReserve (= 2) Резервы за спонсируемого: owner-резервы создаваемых им объектов и резервы аккаунтов (включая спонсируемое создание аккаунта)

У каждого типа транзакций появляются три общих поля: Sponsor, SponsorFlags и (когда спонсорство этого требует) SponsorSignature — вложенный not-signing STObject поверх того же преимиджа, что и основная подпись. Допустимы две альтернативные формы: одиночная подпись (SigningPubKey + TxnSignature) либо мультисиг спонсора (вложенный массив Signers).


Ключевые понятия

Объект леджера Sponsorship

Создаётся транзакцией SponsorshipSet, один на пару спонсор/спонсируемый:

Поле Значение
Owner Спонсирующий аккаунт
Sponsee Спонсируемый аккаунт
FeeAmount Остаток XRP-бюджета на спонсируемые комиссии
RemainingOwnerCount Сколько ещё объектов спонсор покроет резервами

Режим обязательной подписи

По умолчанию спонсируемый тратит бюджет без участия спонсора. Флаги SponsorshipSet переключают это по-измеренно:

  • tfSponsorshipSetRequireSignForFee / tfSponsorshipClearRequireSignForFee
  • tfSponsorshipSetRequireSignForReserve / tfSponsorshipClearRequireSignForReserve

Если подпись обязательна, транзакция спонсируемого должна нести валидный SponsorSignature — см. Сценарии подписания.

Учётные поля

  • AccountRoot: SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount
  • RippleState: HighSponsor / LowSponsor — кто покрывает резерв каждой стороны трастлайна

Типы транзакций

SponsorshipSet

Создаёт/обновляет отношение либо удаляет его флагом tfDeleteObject. По rippled SponsorshipSet::preflight присутствует ровно одно из полей Sponsee / CounterpartySponsor (отправитель — другая сторона); создавать и обновлять может только спонсор, а удалять — любая из сторон:

// Спонсор создаёт/обновляет (указывает спонсируемого):
var setup = new SponsorshipSet
{
    Account = sponsor.ClassicAddress,
    Sponsee = sponsee.ClassicAddress,
    FeeAmount = new Currency { ValueAsXrp = 5m },
    RemainingOwnerCount = 3,
};
setup = await client.Autofill(setup);
await client.SubmitAndWait(setup, sponsor, true);

// Спонсируемый сам удаляет своё спонсорство (указывает спонсора);
// при удалении запрещены модификационные флаги и FeeAmount/MaxFee/RemainingOwnerCount:
var deletion = new SponsorshipSet
{
    Account = sponsee.ClassicAddress,
    CounterpartySponsor = sponsor.ClassicAddress,
    Flags = SponsorshipSetFlags.tfDeleteObject,
};
deletion = await client.Autofill(deletion);
await client.SubmitAndWait(deletion, sponsee, true);

SponsorshipTransfer

Создаёт, передаёт или завершает спонсорство резерва уже существующих объектов леджера. Обязателен ровно один режимный флаг (по rippled SponsorshipTransfer::preflight; для Create/Reassign целевой владелец — Account, поле Sponsee там запрещено):

Флаг Кто отправляет Что делает Дополнительные поля
tfSponsorshipCreate спонсируемый (владелец неспонсируемых объектов) устанавливает спонсорство резерва существующих объектов Sponsor (новый спонсор) + spfSponsorReserve обязательны; спонсор со-подписывает
tfSponsorshipReassign спонсируемый (владелец спонсируемых объектов) переносит резерв с текущего спонсора на нового Sponsor (новый спонсор) + spfSponsorReserve обязательны; новый спонсор со-подписывает
tfSponsorshipEnd спонсируемый или спонсор снимает спонсорство резерва (цель = Sponsee, если указан, иначе Account) Sponsor и SponsorFlags запрещены

Пошагово: создание спонсорства

XrplWallet sponsor = XrplWallet.Generate();
XrplWallet sponsee = XrplWallet.Generate();
// оба кошелька предварительно пополнить

var tx = new SponsorshipSet
{
    Account = sponsor.ClassicAddress,
    Sponsee = sponsee.ClassicAddress,
    FeeAmount = new Currency { ValueAsXrp = 5m },
    RemainingOwnerCount = 3,
};
tx = await client.Autofill(tx);
TransactionSummary result = await client.SubmitAndWait(tx, sponsor, true);
// result.Meta.TransactionResult == "tesSUCCESS"

Проверка через account_objects:

var request = new AccountObjectsRequest(sponsor.ClassicAddress) { Type = LedgerEntryType.Sponsorship };
var objects = await client.AccountObjects(request);
LOSponsorship sponsorship = objects.AccountObjectList.OfType<LOSponsorship>().First();

Отправка спонсируемой транзакции

Спонсируемый отправляет любую обычную транзакцию с полями спонсорства:

var payment = new Payment
{
    Account = sponsee.ClassicAddress,
    Destination = destination.ClassicAddress,
    Amount = new Currency { ValueAsXrp = 1m },
    Sponsor = sponsor.ClassicAddress,
    SponsorFlags = SponsorCoverage.spfSponsorFee,
};
payment = await client.Autofill(payment);

Если спонсорство не требует co-подписи — подписывайте и отправляйте как обычно. Если требует — один из сценариев ниже.


Сценарии подписания (V1/V2/V3)

SponsorSignature подписывается над тем же преимиджем, что и основная подпись (аналогично counterparty-паттерну LoanSet).

Простой путь — стандартные Sign/Submit (10.8.0+)

Выбирать хелпер не нужно: стандартный API маршрутизирует по роли. Кошелёк, совпадающий с tx.Sponsor, создаёт подпись спонсора; кошелёк отправителя подписывает основную подпись, сохраняя уже присутствующий SponsorSignature; умный SubmitAndWait компонует, сверяет require-sign флаги спонсорства с леджером и отправляет.

// Декодирует переданный blob обратно в транзакцию для подписи
static Dictionary<string, object> Reparse(string blob) =>
    JsonSerializer.Deserialize<Dictionary<string, object>>(
        XrplBinaryCodec.Decode(blob).ToJsonString(), XrplJsonOptions.Default);

// Оба ключа локально — один вызов:
await client.SubmitAndWaitSponsored(payment, sponseeWallet, sponsorWallet);

// Ключи на разных устройствах — каждая сторона просто вызывает Sign:
var sponsorPart = sponsorWallet.Sign(preparedTx);               // добавит SponsorSignature
var final = sponseeWallet.Sign(Reparse(sponsorPart.TxBlob));    // добавит основную подпись
await client.SubmitRequest(final.TxBlob, true);

// Либо отправляющая сторона финализирует всё сама:
await client.SubmitAndWait(partiallySignedTx, sponsorWallet); // спонсор доводит tx, подписанную спонсируемым

При отсутствии обязательной подписи SubmitAndWait падает сразу с "transaction is not signed by all participants" вместо ошибки от ноды. Мультисиг любой из сторон остаётся переносимым: устройства подписывают с multisign: true, а client.ComposeSignatures(parts) раскладывает Signer-записи по секциям согласно SignerList'ам леджера (с пре-чеком кворума по весам).

Advanced — явные хелперные сценарии

V1 — Автоматический (оба ключа в одном процессе):

JsonObject prepared = SponsorSigningHelper.PrepareForSigning(payment, sponseeWallet);
SignatureResult signed = SponsorSigningHelper.SignSponsored(prepared, sponseeWallet, sponsorWallet);
await client.SubmitRequest(signed.TxBlob, true);

V2 — Параллельный (ключи на разных устройствах, объединение позже):

var sponsorSig  = sponsorWallet.SignAsSponsor(prepared);
var submitterSig = sponseeWallet.Sign(prepared);
var combined = SponsorSigningHelper.CombineSponsorSignatures(submitterSig.TxBlob, sponsorSig.TxBlob);
await client.SubmitRequest(combined.TxBlob, true);

V3 — Последовательный (сначала спонсор, затем отправитель финализирует):

var withSponsor = sponsorWallet.SignAsSponsor(prepared);
var final = SponsorSigningHelper.SubmitterSign(withSponsor.TxBlob, sponseeWallet);
await client.SubmitRequest(final.TxBlob, true);

Спонсорство внутри Batch

Спонсорство сочетается с Batch (XLS-56); правила зеркалят rippled Batch::preflight:

  • Спонсирование резерва внутренней транзакции: на inner ставятся Sponsor + spfSponsorReserve и пустой объект SponsorSignature как маркер. Маркер делает спонсора обязательным batch-подписантом — спонсор авторизует весь батч через стандартный Sign: одиночной подписью или через свой SignerList (вложенный мультисиг BatchSigner.Signers). Подписные данные внутрь маркера не кладутся никогда.
  • Спонсирование комиссии внешнего батча: Sponsor + spfSponsorFee на внешнем Batch; спонсор со-подписывает сам батч обычной SponsorSignature через стандартный Sign.
  • Запрещено протоколом (ValidateBatch отсекает на клиенте): spfSponsorReserve на внешнем Batch, спонсирование комиссии на inner-транзакциях, подписные данные внутри маркеров и любые Loan/Vault транзакции как inner (rippled kDisabledTxTypes → temINVALID_INNER_BATCH) — со-подпись заёмщика LoanSet внутри Batch невозможна.
// Внутренний TrustSet со спонсированным резервом; спонсор подписывает через
// свой SignerList (Reparse — см. «Простой путь» выше)
SignatureResult holderPart  = holder.Sign(batchDict);
SignatureResult signer1Part = signer1.Sign(Reparse(holderPart.TxBlob),  multisign: true, signingFor: sponsor.ClassicAddress);
SignatureResult signer2Part = signer2.Sign(Reparse(signer1Part.TxBlob), multisign: true, signingFor: sponsor.ClassicAddress);
SignatureResult final       = root.Sign(Reparse(signer2Part.TxBlob));

Комиссии

По rippled Transactor::calculateBaseFee одиночная SponsorSignature не добавляет к базовой комиссии ничего. Надбавка возникает только при мультисиге спонсора: каждый подписант, вложенный в SponsorSignature.Signers, добавляет одну базовую комиссию. Autofill SDK учитывает это автоматически.


Объекты леджера

  • LOSponsorship — объект отношения (см. Ключевые понятия)
  • LOAccountRoot.SponsoredOwnerCount / SponsoringOwnerCount / SponsoringAccountCount
  • LORippleState.HighSponsor / LowSponsor

Тестирование

Амендмент есть только на develop. Репозиторий содержит nightly-стенд со включённым на генезисе Sponsor:

docker compose -f .ci-config/docker-compose.batchv11.yml up -d --build
dotnet test Tests/Xrpl.Tests/Xrpl.Tests.csproj --settings test.runsettings --filter "TestISponsorship"

Интеграционные тесты защищены AmendmentGuard: на ноде без амендмента они завершаются как inconclusive, а не падают. Полные рабочие примеры — Tests/Xrpl.Tests/Integration/transactions/TestISponsorship.cs; детали стендов — гайд по standalone-ноде.


Типичные ошибки

Ошибка Значение
temDISABLED Амендмент Sponsor не активен на ноде
tecNO_SPONSOR_PERMISSION Спонсорство не покрывает это измерение, бюджет исчерпан либо отсутствует/невалиден обязательный SponsorSignature
terNO_PERMISSION Состояние отношения не допускает операцию (например, передача чужого спонсорства)
Валидация: "invalid Sponsor" Поле Sponsor — не строковый адрес (клиентская проверка)

English version: Sponsorship-Guide

  • Edit this page
In this article
Back to top Generated by DocFX