Repository navigation
feat(contract): implement invoice-registry create_invoice with full schema - #70
Merged
codemagician1949 merged 1 commit intoAug 25, 2026
Conversation
…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
Contributor
|
Nice job @oluwagbemigaphilip |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
create_invoice, the entrypoint SMEs should use to raise an invoicewith 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, SMEaddress, 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.
TermsKeycarries a fixed marker field for the same reasonVerifierKeydoes:
#[contracttype]encodes a tuple struct as a bare XDR vec of itsfields with no type-name discriminant, so a single-
Symbolkey would occupythe same ledger slot as
InvoiceKeyfor the same id and silently clobberthe 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 theissue'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
registerpath cannot be silently re-created here.Event
Emits
("inv_new", id)with(sme, buyer, commitment, due_date). The id isa 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 doccomments carry what callers need.
Verification
cargo test --workspace— 153 tests passing, 14 of them newcargo build --release --target wasm32-unknown-unknown --workspaceclean;invoice_registry.wasmis 52,590 bytes against the 64KB CI limitNew 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-
registercase andthat 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_authin the Soroban test host aborts the process rather than unwinding, so
#[should_panic]cannot observe it and the whole test binary dies. Theauthorization is asserted instead by inspecting
env.auths().Closes #13