Skip to content

Commit a73d2de

Browse files
committed
feat(contracts): per-type ContractScope (on-chain/off-chain) as a first-class property
Replaces the scattered `Type == "Boarding"` checks that implicitly decided whether a contract's funds live on-chain (boarding UTXOs) or off-chain (VTXOs). - New `[Flags] ContractScope { Onchain = 1, Offchain = 2 }` (Both = Onchain|Offchain). - `ArkContract.DefaultScope` (abstract — every type must declare its scope, so a new type can't silently inherit a wrong default): Boarding => Onchain; Payment, HashLockPayment, Delegate, VHTLC, Note, Unknown, Generic => Offchain. - Resolved scope persisted on `ArkContractEntity.Scope` / `ArkWalletContractEntity` (indexed). `ToEntity(scopeOverride:)` is the per-instance escape hatch (Scope = scopeOverride ?? DefaultScope); nothing sets it yet. - `IContractStorage.GetContracts(scope:)` filters via the SQL-translatable bitwise predicate `(Scope & s) == s` (NOT HasFlag, which EF can't translate). - Consumers migrated off the boarding-type check: BoardingUtxoPollService, BoardingUtxoSyncService, OnchainSweepService -> scope: Onchain; WalletRecoveryService -> offchain bitwise filter. Tests (TDD): per-type DefaultScope, ToEntity override resolution, flags semantics, and a SQLite-backed GetContracts(scope:) test (real SQLite, not InMemory — InMemory client-evaluates and would hide a non-translatable query). Schema note: NArk.Storage.EfCore is provider-agnostic with no migrations history — the Scope column ships via entity configuration. Consumers that manage their own schema with EF migrations should add a migration creating the column (default Offchain) and backfilling existing boarding rows to Onchain.
1 parent 569bfcf commit a73d2de

23 files changed

Lines changed: 433 additions & 7 deletions

NArk.Abstractions/Contracts/ArkContract.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ public abstract class ArkContract(OutputDescriptor server)
1111

1212
public abstract string Type { get; }
1313

14+
/// <summary>
15+
/// The layer this contract type's funds live on by default (on-chain vs off-chain).
16+
/// Abstract so every contract type makes an explicit, compile-time scope decision.
17+
/// Used as the fallback when <see cref="ToEntity"/> is called without a scope override.
18+
/// </summary>
19+
public abstract ContractScope DefaultScope { get; }
20+
1421
public OutputDescriptor? Server { get; } = server;
1522

1623
public virtual ArkAddress GetArkAddress(OutputDescriptor? defaultServerKey = null)
@@ -53,11 +60,24 @@ public virtual Script GetScriptPubKey()
5360
return spendInfo.OutputPubKey.ScriptPubKey;
5461
}
5562

63+
/// <summary>
64+
/// Projects this contract into a persistable <see cref="ArkContractEntity"/>.
65+
/// </summary>
66+
/// <param name="walletIdentifier">The wallet that owns the contract.</param>
67+
/// <param name="defaultServerKey">Reserved for address derivation parity; not persisted.</param>
68+
/// <param name="createdAt">Creation timestamp; defaults to now.</param>
69+
/// <param name="activityState">Initial activity state; defaults to Active.</param>
70+
/// <param name="scopeOverride">
71+
/// Per-instance scope override. When null, the entity's effective scope is
72+
/// <see cref="DefaultScope"/>. The override is write-time only — the persisted
73+
/// column always means "this contract's effective scope."
74+
/// </param>
5675
public ArkContractEntity ToEntity(
5776
string walletIdentifier,
5877
OutputDescriptor? defaultServerKey = null,
5978
DateTimeOffset? createdAt = null,
60-
ContractActivityState activityState = ContractActivityState.Active)
79+
ContractActivityState activityState = ContractActivityState.Active,
80+
ContractScope? scopeOverride = null)
6181
{
6282
return new ArkContractEntity(
6383
GetScriptPubKey().ToHex(),
@@ -66,7 +86,10 @@ public ArkContractEntity ToEntity(
6686
GetContractData(),
6787
walletIdentifier,
6888
createdAt ?? DateTimeOffset.UtcNow
69-
);
89+
)
90+
{
91+
Scope = scopeOverride ?? DefaultScope
92+
};
7093
}
7194

7295
protected abstract IEnumerable<ScriptBuilder> GetScriptBuilders();

NArk.Abstractions/Contracts/ArkContractEntity.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ DateTimeOffset CreatedAt
1414
/// Separate from AdditionalData which stores contract crypto params.
1515
/// </summary>
1616
public Dictionary<string, string>? Metadata { get; init; }
17+
18+
/// <summary>
19+
/// The effective layer this contract's funds live on (on-chain vs off-chain).
20+
/// Resolved at write time from the contract's <c>DefaultScope</c> or an explicit override.
21+
/// </summary>
22+
public ContractScope Scope { get; init; }
1723
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace NArk.Abstractions.Contracts;
2+
3+
/// <summary>
4+
/// Identifies the layer a contract's funds live on: on-chain (a boarding UTXO,
5+
/// synced/swept via Esplora/NBXplorer) or off-chain (a VTXO, synced via the
6+
/// Arkade indexer). A contract whose funds can appear on both layers uses
7+
/// <see cref="Onchain"/> | <see cref="Offchain"/>.
8+
/// </summary>
9+
/// <remarks>
10+
/// Query this only via the bitwise form <c>(Scope &amp; X) == X</c>, which EF Core
11+
/// translates to SQL across Npgsql, SQLite and SqlServer. A value carrying both
12+
/// flags therefore satisfies an <see cref="Onchain"/>-include and an
13+
/// <see cref="Offchain"/>-include filter alike. Never use <c>HasFlag</c>: EF Core
14+
/// does not translate it (it client-evaluates or throws).
15+
/// </remarks>
16+
[Flags]
17+
public enum ContractScope
18+
{
19+
/// <summary>Funds live on-chain (a boarding UTXO).</summary>
20+
Onchain = 1,
21+
22+
/// <summary>Funds live off-chain (a VTXO synced via the Arkade indexer).</summary>
23+
Offchain = 2,
24+
}

NArk.Abstractions/Contracts/IContractStorage.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public interface IContractStorage : IActiveScriptsProvider
1717
/// <param name="searchText">Filter by script containing this text. If null, no text filter.</param>
1818
/// <param name="skip">Number of records to skip (for pagination). If null, no skip.</param>
1919
/// <param name="take">Number of records to take (for pagination). If null, no limit.</param>
20+
/// <param name="scope">
21+
/// Filter by contract scope (on-chain/off-chain) via bitwise include
22+
/// <c>(Scope &amp; scope) == scope</c>. If null, no scope filter.
23+
/// </param>
2024
/// <param name="cancellationToken">Cancellation token.</param>
2125
Task<IReadOnlyCollection<ArkContractEntity>> GetContracts(
2226
string[]? walletIds = null,
@@ -26,6 +30,7 @@ Task<IReadOnlyCollection<ArkContractEntity>> GetContracts(
2630
string? searchText = null,
2731
int? skip = null,
2832
int? take = null,
33+
ContractScope? scope = null,
2934
CancellationToken cancellationToken = default);
3035

3136
Task SaveContract(ArkContractEntity walletEntity, CancellationToken cancellationToken = default);

NArk.Core/Contracts/ArkBoardingContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public class ArkBoardingContract(OutputDescriptor server, Sequence exitDelay, Ou
2222
public override string Type => ContractType;
2323
public const string ContractType = "Boarding";
2424

25+
/// <summary>Boarding funds live on-chain as a boarding UTXO.</summary>
26+
public override ContractScope DefaultScope => ContractScope.Onchain;
27+
2528

2629
/// <summary>
2730
/// Boarding contracts use on-chain Bitcoin addresses, not Ark addresses.

NArk.Core/Contracts/ArkDelegateContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public class ArkDelegateContract : ArkContract
1818
public override string Type => ContractType;
1919
public const string ContractType = "Delegate";
2020

21+
/// <summary>Delegate funds live off-chain as a VTXO.</summary>
22+
public override ContractScope DefaultScope => ContractScope.Offchain;
23+
2124
public ArkDelegateContract(
2225
OutputDescriptor server,
2326
Sequence exitDelay,

NArk.Core/Contracts/ArkNoteContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public class ArkNoteContract(uint amount, byte[] preimage) : ArkContract(null!)
1919
public override string Type => ContractType;
2020
public const string ContractType = "arknote";
2121

22+
/// <summary>Arkade note funds live off-chain as a VTXO.</summary>
23+
public override ContractScope DefaultScope => ContractScope.Offchain;
24+
2225
protected override IEnumerable<ScriptBuilder> GetScriptBuilders()
2326
{
2427
yield return CreateClaimScript();

NArk.Core/Contracts/ArkPaymentContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class ArkPaymentContract(OutputDescriptor server, Sequence exitDelay, Out
2121
public override string Type => ContractType;
2222
public const string ContractType = "Payment";
2323

24+
/// <summary>Payment funds live off-chain as a VTXO.</summary>
25+
public override ContractScope DefaultScope => ContractScope.Offchain;
26+
2427

2528
protected override IEnumerable<ScriptBuilder> GetScriptBuilders()
2629
{

NArk.Core/Contracts/GenericArkContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public class GenericArkContract(OutputDescriptor server, IEnumerable<ScriptBuild
88
{
99
public override string Type { get; } = "generic";
1010

11+
/// <summary>Generic contracts default to off-chain (VTXO) unless overridden at write time.</summary>
12+
public override ContractScope DefaultScope => ContractScope.Offchain;
13+
1114
protected override IEnumerable<ScriptBuilder> GetScriptBuilders()
1215
{
1316
return scriptBuilders;

NArk.Core/Contracts/HashLockedArkPaymentContract.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public byte[] Hash
4040

4141
public override string Type => ContractType;
4242
public const string ContractType = "HashLockPaymentContract";
43+
44+
/// <summary>Hash-locked payment funds live off-chain as a VTXO.</summary>
45+
public override ContractScope DefaultScope => ContractScope.Offchain;
4346
public byte[] Preimage => preimage;
4447
public Sequence ExitDelay => _exitDelay;
4548
public HashLockTypeOption HashLockType => hashLockType;

0 commit comments

Comments
 (0)