Skip to content

feat!: add Tron support and replace walletAddress with activeWallet - #49

Draft
tolgahan-arikan wants to merge 30 commits into
masterfrom
add-tron-support
Draft

tolgahan-arikan wants to merge 30 commits into
masterfrom
add-tron-support

Conversation

@tolgahan-arikan

@tolgahan-arikan tolgahan-arikan commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds Tron wallet support to the Swift SDK, matching the TypeScript SDK (0xPolygon/oms-wallet-typescript-sdk#104): create, import and sign in with Tron wallets, sign and verify, send TRX and TRC-20 transfers, and read balances from the OMS Tron indexer. It also replaces walletAddress with a typed activeWallet and makes session optional. This is a breaking change; see MIGRATION.md (0.4.0). A separate commit adds HPKE tests for wallet import.

Changes

Tron (feat! commit)

  • Generated client: waas.gen.swift is copied unchanged from upstream waas origin/master @ 8a2c7f6 (proto/clients/waas.gen.swift, schema v1-26.9.22-f21ed9d2), generated with webrpc-gen -schema=schema/waas.ridl -target=github.com/webrpc/gen-swift@v0.1.4 -client -visibility=internal -namespace=WaasGenerated -out=./clients/waas.gen.swift, the same command this repo uses.
  • Wallets and networks: WalletType.tron, TronNetwork / TronNetworks (tron:mainnet, tron:nile). Tron import (.tron / .tronBytes) uses the same secp256k1 key format as Ethereum.
  • Transactions: sendTronTransaction and callTronContract, always native mode. data is forwarded exactly: nil sends a plain TRX transfer, "0x" calls the payable fallback.
  • Signing: signTronMessage, signTronTypedData, isValidTronMessageSignature and isValidTronTypedDataSignature (networkFamily: tron).
  • Balances: IndexerClient.getTronBalances calls /v1/TronIndexerGateway/GetTokenBalancesDetails, with the URL derived from the publishable key like the Solana gateway. Models mirror the Solana ones. Fee-option balance lookup uses it for unsponsored Tron fees.
  • Wallet-type checks: methods check the stored wallet type instead of the address shape, before any request.
  • Contract method names: callContract and callTronContract reject a full signature ("transfer(address,uint256)") locally. WaaS only accepts the bare name and builds the signature from args. The README's EVM example had the wrong format and is fixed.
  • Wallet responses: an Ethereum wallet whose address isn't valid hex now fails with .invalidResponse.

Breaking changes (0.4.0)

  • WalletClient.walletAddress is replaced by activeWallet: Wallet?.
  • OMSWalletSessionState is renamed to OMSWalletSession; session is optional, with non-optional expiresAt and auth.
  • walletAddress is removed from the wallet activation result (now WalletActivationResult, see below) and CompleteAuthResult (.walletSelected(wallet:wallets:credential:)). The session-expired event gains wallet.
  • The stored session record moves to version 2 and stores the full wallet. Version-1 records are discarded, so users sign in once after upgrading.
  • An auth response whose credential expiresAt can't be parsed now fails with .invalidResponse, since expiresAt is no longer optional.

Docs and demos

  • README.md (Tron section), MIGRATION.md, docs/error-contracts.md, plus regenerated API.md and the public API baseline.
  • SDK demo: an Ethereum/Tron picker on login. Tron wallets get Wallet, Send and Sign tabs on Nile: TRX and USDT balances with faucet links; TRX / USDT / custom TRC-20 sends that show "Transaction submitted."; Tronscan links.
  • Trails Actions demo: EVM-only, so it treats a non-Ethereum active wallet as signed out.

HPKE tests (test commit, no release impact)

  • A round-trip test: the SDK's hand-written HPKE seals, and CryptoKit's HPKE.Recipient opens. A tampered ciphertext is rejected. Runs on macOS 14+ / iOS 17+.
  • An RFC 9180 test-vector test (DHKEM(P-256, HKDF-SHA256) / HKDF-SHA256 / AES-256-GCM, base mode, from the CFRG test-vectors.json). It checks enc, shared secret, key schedule, key, base nonce and ciphertext through a new internal P256HPKE.seal(...ephemeralPrivateKey:info:aad:) hook. The public seal path is unchanged.

Differences from the TypeScript SDK

  • value is a String in sun, like other Swift amounts.
  • The Tron gateway Webrpc header uses gen-swift, like the Swift Solana header.

Cross-SDK API alignment (refactor! and feat(demo) commits)

Aligns the public API with the TypeScript and Kotlin SDKs after a cited three-way API review. Breaking changes are made directly, with no deprecated aliases, and each one has a MIGRATION.md 0.4.0 entry.

  • Signature verification: all isValid…Signature methods take an optional walletAddress and no longer take walletId. When the address is omitted, the active wallet's address is used. An explicit address no longer requires a session, and requests never send a wallet ID.
  • contractAddress: replaces contract: on callContract and callTronContract; callContract's args defaults to nil.
  • WalletActivationResult replaces WalletSelectionResult.
  • Session timestamps: OMSWalletSession.expiresAt and OMSWalletSessionExpiredEvent.expiredAt are now ISO-8601 Strings.
  • Removed:
    • the public walletId, Network.chainId and the decimals = 18 default of parseUnits/formatUnits;
    • the polygonAmoy case, which is now amoy, along with its alias and the "polygonamoy" lookup.
  • Error codes: the new .walletAddressAlreadyImported maps WaaS error 7313 to status 409.
  • String values: operation values use Oidc casing (wallet.startOidcRedirectAuth, wallet.handleOidcRedirectCallback), and upstream service values are waas/indexer.
  • Additions:
    • shared accessors on TronBalance and SolanaBalance;
    • a public WalletImportPrivateKey.walletType;
    • WalletClient.defaultSessionLifetimeSeconds and maxSessionLifetimeSeconds.
  • Parameter order: startOIDCRedirectAuth now uses the same order as Kotlin and TypeScript.
  • Docs fixes: the README signing key type is ecdsa-p256-sha256, and the Tron balance wording is corrected. AGENTS.md describes the new verification rule.
  • Demo: Solana Devnet support (SOL and USDC balances, sign/verify, transfers with fee options) alongside Ethereum and Tron.
  • Changes with no compile error, all covered in MIGRATION:
    • the operation and service string values;
    • the already-imported error code;
    • verification with an address no longer needs a session;
    • findByName("polygonamoy") now returns nil;
    • the expiry timestamps interpolate as ISO strings.

Review fixes and cleanup

After an outsider review and a simplicity review, every finding was re-checked against the code before changing it. The commits fall into these groups:

  • Fixes:

    • sign-in now rejects a wallet with an unknown type or key origin with .invalidResponse; before, it was accepted and then silently signed out on every relaunch, deleting the signer key;
    • an unparsable credential expiresAt in email auth now gets the same sign-out cleanup as other post-auth failures;
    • a given but empty or whitespace-only walletAddress in the isValid…Signature methods throws .validationError before any request, as in TypeScript and Kotlin;
    • MIGRATION names the removed CompleteAuthResult.walletAddress;
    • in the demo, reauth keeps the wallet family, and stale Tron balances are cleared on error.
  • Cleanup of code this PR added:

    • Solana balances map an empty imageUrl/metadataUri to nil, like Tron, TypeScript and Kotlin (MIGRATION note);
    • signature verification checks the session before the wallet type, so an expired session gives sessionExpired, as in TypeScript;
    • the stored session record requires expiresAt;
    • unused wallet-type mappings removed;
    • Solana/Tron fee-balance enrichment is shared;
    • walletId is derived from activeWallet.
  • Compat leftovers removed (0.4.0 is breaking):

    • indexer models read and write only the gateway's field names (balance, tokenID, metaTxnID, tokenIds, tokenId), dropping old alternate spellings the gateway never sends;
    • MIGRATION notes the changed JSON keys of TransactionTransfer, TokenMetadata and TokenMetadataAsset;
    • the README upgrade pointer now targets 0.4.0.
  • Cleanup of code already on master:

    • unused helpers and error cases removed;
    • completeWalletAuth, clearSession and WalletCredentialSession simplified;
    • HKDF uses CryptoKit, with byte-identical output;
    • duplicate test aliases removed;
    • the generated-WaaS leak check reads real request type names from waas.gen.swift, where it previously listed names that don't exist;
    • small demo cleanups.
  • Docs pass:

    • MIGRATION covers the blank walletAddress validation, unparsable credential expiry and the ContractTokenBalance key;
    • the README balance examples check for an Ethereum wallet, and the Tron example uses a placeholder recipient;
    • AGENTS.md and the contributor docs say API.md is generated by scripts/generate-api.sh.

The public API baseline is unchanged by these commits.

Stale sign-in fix (fix commit)

A sign-in attempt that failed after a newer sign-in had completed signed out unconditionally, and cleared the newer session, its signer key and its stored record. Two paths did this: an unreadable credential expiresAt, and a later wallet-listing or activation failure. Failed attempts now clear the session only while it is still the one they started from. Covered by TestStaleInvalidAuthResponseKeepsNewerSession.

Test audit (test commits)

An audit of every test: four test-only commits. Every new or rewritten test was checked to fail when the behaviour it covers is broken. A test was removed or merged only when its partner already asserts everything it checked.

  • New coverage:
    • the delayed session-expiry timer (previously only the already-expired branch ran);
    • OIDC callbacks that must not consume the pending sign-in;
    • the sponsored fee selector being called;
    • both true and false signature-verification results.
  • Count: 168 → 145 tests.
  • No production code changed.

Testing

  • swift build passes
  • swift test passes (145 tests)
  • SDK demo app builds
  • Trails Actions demo app builds
  • scripts/verify.sh passes (API generator tests, public API check, API.md check, pod lib lint, both demo builds)

Not tested by hand yet: the demo's Tron and Solana flows with a signed-in session (the TypeScript example's Tron flows were tested against dev on Nile).

Related

Port Tron support from the TypeScript SDK (0xPolygon/oms-wallet-typescript-sdk#104).

- Regenerate the WaaS client from waas 8a2c7f6 (schema v1-26.9.22-f21ed9d2): adds
  PrepareTronTransaction, PrepareTronContractCall, the `tron` wallet type and network
  family, and `networkFamily` on IsValidTypedDataSignature.
- Add WalletType.tron, TronNetwork/TronNetworks (tron:mainnet, tron:nile),
  sendTronTransaction (native mode, sun values, `data` forwarded as given),
  callTronContract, signTronMessage, signTronTypedData, isValidTronMessageSignature,
  isValidTronTypedDataSignature, and Tron private key import.
- Add IndexerClient.getTronBalances through the OMS Tron indexer gateway and use it
  for Tron fee-option balances so FeeOptionSelector.firstAvailable works for
  unsponsored Tron fees.
- Guard wallet-family methods by the stored wallet type instead of address shape.
- Reject contract method names that are not bare function names before any request.
- Validate Ethereum wallet addresses in wallet responses (shape only).

BREAKING CHANGE: WalletClient.walletAddress is replaced by activeWallet (Wallet?).
OMSWalletSessionState is renamed to OMSWalletSession; WalletClient.session is now
optional with non-optional expiresAt and auth, and is non-nil exactly when
activeWallet is. walletAddress is removed from WalletSelectionResult and
CompleteAuthResult.walletSelected. OMSWalletSessionExpiredEvent gains `wallet`.
Stored sessions now persist the full wallet (record version 2); version 1 records
are discarded on load, so users sign in once after upgrading.
- Open SDK-sealed payloads with CryptoKit's HPKE.Recipient (macOS 14+/iOS 17+),
  including a tampered-ciphertext rejection.
- Check the hand-written DHKEM(P-256, HKDF-SHA256)/HKDF-SHA256/AES-256-GCM base
  mode against the official CFRG test vector referenced by RFC 9180: enc, shared
  secret, key schedule context, secret, key, base nonce, and ciphertext.
- Add an internal P256HPKE hook that takes the ephemeral key, info, and aad so the
  vector can be reproduced; the public seal path is unchanged.
… verification addresses

- Sign-in no longer activates a wallet with an unknown network family or key
  origin that session restore would discard; it throws invalidResponse.
- completeEmailAuth signs out when the auth response has an unreadable
  credential expiresAt, like the other post-auth failures.
- isValid*Signature methods reject an empty or whitespace-only walletAddress
  with validationError before any request.
…nces

The sdk-demo now picks the wallet type from the restored session and from
OMSWalletSessionExpiredEvent.wallet, so signing in again after expiry returns
to the same Tron, Solana, or Ethereum wallet. A Nile balance error now resets
the TRX and USDT values like the Solana panel does.
Move decodeNonEmptyString to a KeyedDecodingContainer extension and use it
for the Solana balance structs too, so an empty imageUrl or metadataUri
decodes as nil for both families, as in the TypeScript and Kotlin SDKs.
requireActiveWalletType now runs the expiry/session check first and returns
the active wallet, so verificationWalletAddress no longer calls
requireActiveWalletId just for its side effect. An expired session with a
wallet of another family now throws sessionExpired rather than
validationError, matching the TypeScript SDK.
Every version 2 record is written with an expiry, so decode it as required
and drop the redundant CodingKeys enum. A record without expiresAt is still
discarded on load, as before.
Wallets map through networkFamily; WalletType.waasValue and
WaasGenerated.WalletType.sdkValue had no callers.
The two gateway families now differ only in their indexer call; address
deduplication and the FeeOptionWithBalance mapping are shared helpers. The
EVM path uses the same dedupe and a plain [String: TokenBalance] map instead
of a double optional.
walletId was stored separately but always set alongside activeWallet. It is
now computed from activeWallet, as in the Kotlin SDK, and tests seed the
active wallet instead of a bare wallet ID.
Drop clearAllPendingOIDCRedirectAuth, ByteUtils.hexToBytes, the always-nil
contractTypes/contractBlacklist EVM filter fields, the unused
jsonString(pretty:) parameter, the never-thrown
TransactionError.pollingTimedOut and .missingTransactionHash cases, and the
unreachable second noFeeOptionsAvailable guard. Wire requests are unchanged.
signOutOnFailure now takes the OIDC redirect ownership and skips the
sign-out itself when a redirect callback owns the flow, replacing three
copies of the ownership == nil branch.
The storage-error wrapping moves into the clearState closure, so the
OIDC-store and plain paths no longer repeat it.
WalletCredentialSession had a line-for-line copy of the ISO-8601 parser and
a makeCredentialSigner wrapper around signerFactory; both are gone. The
restore-time expiry clock is unchanged.
Replace the hand-rolled HMAC extract/expand loop with HKDF<SHA256>, which
is available on the package's macOS 12 / iOS 15 floor and produces the same
output; the RFC 9180 vector tests still pass.
@testable import already exposes the 44 aliases from
Sources/OMSWallet/Generated/WaasGeneratedAliases.swift; keep only the
test-only ones.
The forbidden-symbol pattern listed nine request types that no longer exist
and misspelled GetIDTokenRequest, while current request types were missing.
Request type names are now read from the generated client; the one-item
expected-error loop is a plain check.
- sdk-demo uses WalletClient.maxSessionLifetimeSeconds and
  Network.nativeTokenSymbol instead of local copies.
- sdk-demo AppError drops a cancellation branch and an empty-details check
  that could not run.
- trails-actions drops the unused formatSessionDate.
- MIGRATION: blank walletAddress validation, invalid credential expiresAt,
  ContractTokenBalance tokenID key
- error-contracts: listWallets also rejects malformed Ethereum addresses
- README: guard EVM indexer examples on Ethereum wallets; placeholder Tron recipient
- useWallet/createWallet doc comments describe the persisted wallet; regenerate API.md
- AGENTS/TESTING/publishing/error-contracts: API.md is generated by scripts/generate-api.sh
The Solana screens switch between Devnet and mainnet and the Tron screens between Nile and
mainnet. Each network has its own USDC mint or USDT contract, explorer links and faucet links
(testnets only), and mainnet shows that transfers move real funds. Switching clears the previous
balances and transfer result, and a balance response for a network the screen has left is ignored.
A sign-in attempt that failed after a newer sign-in completed (for example an email-auth response with an unreadable credential expiresAt, or a later wallet-listing failure) signed out unconditionally and cleared the newer session, its signer key and its stored record. Failed attempts now clear the session only while it is still the one the attempt started from.
…onsored selector

- Drive the delayed session-expiry task with a near-future expiry and a movable clock, so the
  timer itself has to fire (previously only the already-expired branch ran).
- Assert ignored OIDC callbacks (unrelated redirect, invalid state) leave the pending flow
  unconsumed and that the real callback still completes sign-in.
- Record sponsored EVM fee-selector invocations instead of asserting inside the closure.
Pin the OIDC ID-token parity vector to a literal token and handle hash computed outside the
SDK, instead of recomputing the expected hash with a helper that mirrors the implementation.
Delete tests whose every assertion is held elsewhere, moving any unique assertion into the
partner first:

- signed-out nil state and custom-provider defaults move into the auth and redirect-start tests
- the Tron signature test's isValid=false case becomes a true/false column of the
  parameterized verification matrix
- tautological error-field, enum raw-value (pinned by API.md), and HPKE length tests go
- session-lifetime validation becomes one test over four entry points and both bounds
- fee-selector parsing, skip-polling, timed-out statuses, ID-token expiry, and key-import
  validation become parameterized tables
- listAccess aggregation, indexer strictness rows, indexer HTTP error cases, and stored-wallet
  validation cases fold into their existing owners
Move exact-duplicate helpers into the shared fixtures in MockWalletTests.swift: one
requestBodyData reader replaces three copies of the URL-protocol bodyData helper (and the dead
MockIndexerURLProtocol copy), activateTestSession backs the three active-session helpers,
testEnvironment replaces the copied default environment, and LockedBox moves out of the Tron
tests. The URL protocols themselves differ in behaviour and stay separate.
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.

1 participant