Skip to content

feat(contract): implement invoice-registry create_invoice with full schema - #70

Merged
codemagician1949 merged 1 commit into
IntensifierLab:mainfrom
oluwagbemigaphilip:feat/invoice-registry-create
Aug 25, 2026
Merged

codemagician1949 merged 1 commit into
IntensifierLab:mainfrom
oluwagbemigaphilip:feat/invoice-registry-create

Conversation

@oluwagbemigaphilip

Copy link
Copy Markdown
Contributor

Adds create_invoice, the entrypoint SMEs should use to raise an invoice
with its commercial terms. It supersedes register, which takes no buyer,
no due date, and authenticates nobody — it records an owner tag on the
caller's word.

Schema and storage

Two records are written under distinct keys:

  • CommittedInvoice, the existing lifecycle record, so every transition,
    freeze, and NFT entrypoint keeps working unchanged.
  • InvoiceTerms, a new record carrying buyer, commitment, due date, SME
    address, and creation timestamp.

They are kept apart rather than merged because the lifecycle record is
rewritten on every status transition while terms are written once and never
mutated — merging would let a transition corrupt the terms, and would have
changed the on-ledger encoding of every record already stored.

TermsKey carries a fixed marker field for the same reason VerifierKey
does: #[contracttype] encodes a tuple struct as a bare XDR vec of its
fields with no type-name discriminant, so a single-Symbol key would occupy
the same ledger slot as InvoiceKey for the same id and silently clobber
the lifecycle record. There is a test pinning that.

The amount is a Pedersen commitment

The issue asks for the amount to be stored. This contract's stated invariant
is that no plaintext amount is ever written on-chain, so the amount is
recorded the way every other amount here is — as a commitment the SME opens
on demand via verify_amount. Storing plaintext would have satisfied the
issue's wording while breaking the property the contract exists to provide.

Validation

Rejects a zero commitment, a due date at or before the current ledger
timestamp (an invoice due now is already defaulted, which the repayment
waterfall has no handling for), and an invoice an SME raised against itself.
Authentication happens before any storage read, so an unsigned call cannot
probe which invoice ids exist by distinguishing error codes.

Duplicates are checked against both records, so an id already taken by the
legacy register path cannot be silently re-created here.

Event

Emits ("inv_new", id) with (sme, buyer, commitment, due_date). The id is
a topic so indexers can filter per invoice without decoding every payload;
the payload carries every field needed to build a row without a follow-up
read.

Contract spec doc limits

Soroban embeds doc comments in the contract spec and caps their length.
Two blocks here exceeded it and made #[contracttype] and #[contractimpl]
panic at expansion with LengthExceedsMax, which surfaces as ~30 cascading
"cannot find type" errors that point nowhere near the cause. The rationale
now lives in plain // comments, which are not embedded, and the doc
comments carry what callers need.

Verification

  • cargo test --workspace — 153 tests passing, 14 of them new
  • cargo build --release --target wasm32-unknown-unknown --workspace clean;
    invoice_registry.wasm is 52,590 bytes against the 64KB CI limit

New tests cover storage under both keys, the key-collision guard, distinct
ids, every validation branch, that a rejected invoice writes nothing and
emits nothing, duplicate rejection including the legacy-register case and
that a duplicate does not overwrite the original, the recorded SME
authorization, and the event's exact topic and payload.

There is deliberately no "unsigned call panics" test: a failed require_auth
in the Soroban test host aborts the process rather than unwinding, so
#[should_panic] cannot observe it and the whole test binary dies. The
authorization is asserted instead by inspecting env.auths().

Closes #13

…chema

Adds `create_invoice`, the entrypoint SMEs should use to raise an invoice
with its commercial terms. It supersedes `register`, which takes no buyer,
no due date, and authenticates nobody — it records an owner tag on the
caller's word.

Schema and storage
------------------
Two records are written under distinct keys:

- `CommittedInvoice`, the existing lifecycle record, so every transition,
  freeze, and NFT entrypoint keeps working unchanged.
- `InvoiceTerms`, a new record carrying buyer, commitment, due date, SME
  address, and creation timestamp.

They are kept apart rather than merged because the lifecycle record is
rewritten on every status transition while terms are written once and never
mutated — merging would let a transition corrupt the terms, and would have
changed the on-ledger encoding of every record already stored.

`TermsKey` carries a fixed marker field for the same reason `VerifierKey`
does: `#[contracttype]` encodes a tuple struct as a bare XDR vec of its
fields with no type-name discriminant, so a single-`Symbol` key would occupy
the *same ledger slot* as `InvoiceKey` for the same id and silently clobber
the lifecycle record. There is a test pinning that.

The amount is a Pedersen commitment
-----------------------------------
The issue asks for the amount to be stored. This contract's stated invariant
is that no plaintext amount is ever written on-chain, so the amount is
recorded the way every other amount here is — as a commitment the SME opens
on demand via `verify_amount`. Storing plaintext would have satisfied the
issue's wording while breaking the property the contract exists to provide.

Validation
----------
Rejects a zero commitment, a due date at or before the current ledger
timestamp (an invoice due now is already defaulted, which the repayment
waterfall has no handling for), and an invoice an SME raised against itself.
Authentication happens before any storage read, so an unsigned call cannot
probe which invoice ids exist by distinguishing error codes.

Duplicates are checked against *both* records, so an id already taken by the
legacy `register` path cannot be silently re-created here.

Event
-----
Emits `("inv_new", id)` with `(sme, buyer, commitment, due_date)`. The id is
a topic so indexers can filter per invoice without decoding every payload;
the payload carries every field needed to build a row without a follow-up
read.

Contract spec doc limits
------------------------
Soroban embeds doc comments in the contract spec and caps their length.
Two blocks here exceeded it and made `#[contracttype]` and `#[contractimpl]`
panic at expansion with `LengthExceedsMax`, which surfaces as ~30 cascading
"cannot find type" errors that point nowhere near the cause. The rationale
now lives in plain `//` comments, which are not embedded, and the doc
comments carry what callers need.

Verification
------------
- `cargo test --workspace` — 153 tests passing, 14 of them new
- `cargo build --release --target wasm32-unknown-unknown --workspace` clean;
  `invoice_registry.wasm` is 52,590 bytes against the 64KB CI limit

New tests cover storage under both keys, the key-collision guard, distinct
ids, every validation branch, that a rejected invoice writes nothing and
emits nothing, duplicate rejection including the legacy-`register` case and
that a duplicate does not overwrite the original, the recorded SME
authorization, and the event's exact topic and payload.

There is deliberately no "unsigned call panics" test: a failed `require_auth`
in the Soroban test host aborts the process rather than unwinding, so
`#[should_panic]` cannot observe it and the whole test binary dies. The
authorization is asserted instead by inspecting `env.auths()`.

Closes IntensifierLab#13
@codemagician1949

Copy link
Copy Markdown
Contributor

Nice job @oluwagbemigaphilip

@codemagician1949
codemagician1949 merged commit 8525ba9 into IntensifierLab:main Aug 25, 2026
3 checks 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.

feat(contract): implement invoice-registry create() with full schema

2 participants