Skip to content

feat(antd-csharp): pointer API (REST + gRPC) - #328

Merged
Nic-dorman merged 1 commit into
mainfrom
nic/v2-1568-pointers-csharp
Oct 19, 2026
Merged

Nic-dorman merged 1 commit into
mainfrom
nic/v2-1568-pointers-csharp

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Part of the pointer API fan-out. Stacked on #326; review that first.

AntdRestClient and AntdGrpcClient gain the daemon's pointer surface:

  • PointerKeysAsync, PointerCreateAsync, PointerUpdateAsync,
    PointerTransferAsync
  • PointerGetAsync, PointerResolveAsync, PointerFinalityAsync,
    PointerControllerAsync
  • PointerPrepareAsync (with PointerPrepareRequest.Create, Update and
    Transfer) and PointerFinalizeAsync, for an external signer

The gRPC client talks to PointerService. Grpc.Tools compiles the protos
at build time, so pointers.proto is added to the csproj's Protobuf list
and no generated code is committed. Both clients are now partial
classes, with the pointer code in their own files.

The methods are also on IAntdClient, with default implementations that
throw NotSupportedException, so a class implementing the interface
outside the package keeps compiling.

Counters are ulong. The daemon sends them as decimal strings, because a
final state's u64::MAX is past what JSON numbers keep exactly in several
languages. They are parsed with NumberStyles.None, so a sign, whitespace
or a non-ASCII digit is an error, never a zero. Finality status is an
enum with an Unknown member for statuses a newer daemon may add, and
RawStatus keeps the wire value. Unset values (null over REST, "" or an
absent message over gRPC) read as null on both transports. Nothing
retries a paid write; the README says each one pays.

Errors map by the daemon's code where it diverges from the status.
PointerFinalException, PointerForkedException and ConflictException
extend ForkException, which every REST 409 has mapped to, so existing
catch blocks still match; a 409 ALREADY_EXISTS stays a plain
ForkException. CloseGroupShortfallException extends AntdException, and
its Retryable flag says a pointer finalize kept the paid state. Over
gRPC the same types come from the x-antd-error-code trailer on
RpcException.Trailers, but only when it agrees with the status code, so
FAILED_PRECONDITION without it is still a PaymentException. The trailer
is read for every call, since a close-group shortfall can come from any
of them. These are new subclasses, so no catch stops compiling, but a
shortfall no longer matches ServiceUnavailableException (REST) or
NetworkException (gRPC); the README's upgrade notes say so.

Tests: 108 new unit tests, 274 in all. They cover every method over a
mock REST daemon and mocked PointerService stubs, the error mapping
both ways including tags on the wrong status, counters up to u64::MAX
and the malformed ones, and the interface's default members.
PointerLiveTests is a round trip against a live daemon on a devnet,
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It
passed against a 10-node devnet:

  • create, update, get, resolve, with bytes fetched back through the
    pointer
  • finality, and a gRPC create, get and finality
  • a transfer, then POINTER_FINAL over both transports
  • a prepare up to the quote, with CONFLICT over both transports while
    it is pending

Example 8 (dotnet run -- 8, or -- pointers) uploads two pieces of
public data, points a pointer at the first, updates it to the second,
resolves it and prints its finality. It takes the owner from
ANTD_POINTER_OWNER and the daemon from ANTD_BASE_URL. It is left out of
"all" because every write pays. It ran clean against the same devnet.

Version bumps happen at release time; nothing here bumps a package version.

🤖 Generated with Claude Code

AntdRestClient and AntdGrpcClient gain the daemon's pointer surface:
- PointerKeysAsync, PointerCreateAsync, PointerUpdateAsync,
  PointerTransferAsync
- PointerGetAsync, PointerResolveAsync, PointerFinalityAsync,
  PointerControllerAsync
- PointerPrepareAsync (with PointerPrepareRequest.Create, Update and
  Transfer) and PointerFinalizeAsync, for an external signer

The gRPC client talks to PointerService. Grpc.Tools compiles the protos
at build time, so pointers.proto is added to the csproj's Protobuf list
and no generated code is committed. Both clients are now partial
classes, with the pointer code in their own files.

The methods are also on IAntdClient, with default implementations that
throw NotSupportedException, so a class implementing the interface
outside the package keeps compiling.

Counters are ulong. The daemon sends them as decimal strings, because a
final state's u64::MAX is past what JSON numbers keep exactly in several
languages. They are parsed with NumberStyles.None, so a sign, whitespace
or a non-ASCII digit is an error, never a zero. Finality status is an
enum with an Unknown member for statuses a newer daemon may add, and
RawStatus keeps the wire value. Unset values (null over REST, "" or an
absent message over gRPC) read as null on both transports. Nothing
retries a paid write; the README says each one pays.

Errors map by the daemon's code where it diverges from the status.
PointerFinalException, PointerForkedException and ConflictException
extend ForkException, which every REST 409 has mapped to, so existing
catch blocks still match; a 409 ALREADY_EXISTS stays a plain
ForkException. CloseGroupShortfallException extends AntdException, and
its Retryable flag says a pointer finalize kept the paid state. Over
gRPC the same types come from the x-antd-error-code trailer on
RpcException.Trailers, but only when it agrees with the status code, so
FAILED_PRECONDITION without it is still a PaymentException. The trailer
is read for every call, since a close-group shortfall can come from any
of them. These are new subclasses, so no catch stops compiling, but a
shortfall no longer matches ServiceUnavailableException (REST) or
NetworkException (gRPC); the README's upgrade notes say so.

Tests: 108 new unit tests, 274 in all. They cover every method over a
mock REST daemon and mocked PointerService stubs, the error mapping
both ways including tags on the wrong status, counters up to u64::MAX
and the malformed ones, and the interface's default members.
PointerLiveTests is a round trip against a live daemon on a devnet,
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It
passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer
- finality, and a gRPC create, get and finality
- a transfer, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT over both transports while
  it is pending

Example 8 (dotnet run -- 8, or -- pointers) uploads two pieces of
public data, points a pointer at the first, updates it to the second,
resolves it and prints its finality. It takes the owner from
ANTD_POINTER_OWNER and the daemon from ANTD_BASE_URL. It is left out of
"all" because every write pays. It ran clean against the same devnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman marked this pull request as ready for review October 16, 2026 09:07
@Nic-dorman
Nic-dorman changed the base branch from nic/v2-1567-antd-pointer-api to main October 16, 2026 12:46

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formal review: APPROVE

Repository: WithAutonomi/ant-sdk, PR #328.
Reviewed head: 27ed742501e354f36e991e73892fc4c569f0ba08. Original merge base: 8e67c611c8f54d032b036bab55bf1b7a2a00b64c.

No material blocker remains in the C# pointer SDK at this head. The omitted optional signed-quote surface is real, but the claim that it prevents external-signer payment/finalize is not supported by the daemon contract. The partial panel returned 15/18 substantive reviews: 6 APPROVE, 7 CHANGES_REQUIRED and 2 UNCERTAIN; this formal verdict follows the documented source adjudication, not a majority-vote count.

Source and test coverage

  • Complete C# pointer interface, models, REST/gRPC adapters, errors, tests and example against the bundled proto and frozen daemon contracts.
  • Reviewed the test source and the exact-head GitHub check result. No local dotnet build or test run was performed; reported author test totals and devnet results are not counted as local evidence.

Findings and adjudication

  • Non-blocking parity gap: PointerModels.cs:259-265,297-308 and both adapters omit include_signed_quotes/signed_quotes. The proto exposes them, but the daemon defines them as opt-in offline-verification material (antd/src/signed_quotes.rs:1-26), defaulting off. Add the optional request flag and returned quotes in a follow-up; document the current limitation.
  • The normal external-signer path remains present: Prepare returns payment triples, payForQuotes supplies tx hashes, and Finalize submits those hashes (AntdGrpcClient.Pointers.cs:85-115; IAntdClient.cs:200-224). Missing optional quote attestations does not remove those fields or prove payment failure.
  • Default interface methods intentionally throw NotSupportedException for old third-party implementations; both sealed concrete clients supply the pointer methods. This is the documented source-compatibility extension, not an unimplemented concrete client.
  • REST typed errors prefer the daemon's structured code; this is the daemon contract, not a defect merely because it is not identical to gRPC tag/status dispatch (Exceptions.cs:332-347).

Panel dissent and its disposition

  • The principal CHANGES_REQUIRED finding is the signed-quote omission. Its presence is confirmed, but payment-blocking severity is refuted by the independent PaymentInfo flow and default-off daemon option. It remains a disclosed API-completeness follow-up, as for PHP #334.
  • The other objections about default interface throws and REST error mapping were checked against the concrete implementations and daemon contract. These were not demonstrated runtime defects. Original seat votes remain in the participation record below.

Review team: actual participation

15/18 scope/role/model seats returned substantive, receipt-validated reviews; 3/3 distinct named models. Partial panel; provisional coverage limits apply to this formal verdict. A receipt is evidence of participation, not proof that its findings are correct. Counts below are per evidence scope, not additional independent models.

  • source-1: glm: correctness=APPROVE, adversarial=CHANGES_REQUIRED; qwen: correctness=CHANGES_REQUIRED, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=CHANGES_REQUIRED.
  • source-2: glm: correctness=APPROVE, adversarial=UNCERTAIN; qwen: correctness=APPROVE, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=CHANGES_REQUIRED.
  • source-3: glm: correctness=UNCERTAIN, adversarial=CHANGES_REQUIRED; qwen: correctness=APPROVE, adversarial=APPROVE; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=APPROVE.
  • Models: GLM 5.3 / OpenRouter z-ai/glm-5.3; local Qwen 3.8 / qwen38-27b; local DS4 Flash / deepseek-v4-flash. Missing seats received the configured same-model retry; no replacement model is counted as an independent vote.

Publication gates and limits

  • Live head rechecked against the reviewed head. Live base cea236950084f8738106cc37247c148f0b411d18: binary diff matches the reviewed diff, merge base is unchanged, no intersecting base-drift paths, and trial merge exited 0.
  • GitHub check snapshot: SUCCESS=1.
  • Partial panel; all three named models contributed, but three scope/role/model seats remained unavailable after retry. No full-panel consensus is claimed.
  • Local dotnet tests and the paid live-pointer flow were not run. The successful GitHub check is a separate evidence channel, not a local run.
  • No paid or live-network tests. No merge, deployment, source change, or Linear state change. This verdict applies only to the exact head above; it is not a fleet-wide or cross-SDK integration sign-off.

@Nic-dorman
Nic-dorman merged commit 0c88edc into main Oct 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants