solrengine-sdp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fac7a0ecbbf6a3c7f9c8f56f21079965c7b33dad55d2f0050223fab13952b39
4
- data.tar.gz: cf05ff3fd6363eaab0739b031d7cd0d7de51775ca5b63b2462bc0b87edf6ed81
3
+ metadata.gz: 15ecef7e5579d87685fb1fe3191d9695a679beeba31fc6df82df0c577477b85e
4
+ data.tar.gz: 76996291b77994894df747da458b0f017df58163be6d80ec7c20ce670b584259
5
5
  SHA512:
6
- metadata.gz: e9fee17c0180f8d2615e3b82179c3d18200b64a579ec041374633905d9d316dc30fa0faea741a55c98d996f336136f51d19aa545a45ec874a84a84267bb02b54
7
- data.tar.gz: 9bcab7a570b2899a883dc4216802e480f5e2d8ee9c1008f31bbb0def8775de424e8aa8ad2251f634eb006faaed65e3c01c993d4ff208527e07ea4f3aa2d8e849
6
+ metadata.gz: e7ec3f5c1f15b148fbb80a18fd46773a44f5632da32b0de1e17196e08a273bc1335e66c46f28916985a6fe0610ebc311296e933fcad6adcb664d13b58d924d59
7
+ data.tar.gz: 641edbdf755534bdf0d64b9ecc3ff72c04a979b471a1a84048b96d4880d96eb7ec0a68e0d60d91620fc7742f9234eb2c1eb28a587bb7ff748ff92a75cf3bd1ac
data/README.md CHANGED
@@ -158,6 +158,36 @@ Rails' default `async` Action Cable adapter delivers broadcasts **in-process onl
158
158
 
159
159
  `Transfer.execute!` runs a SOL balance preflight (`amount + 0.000005` fee buffer) and raises `InsufficientBalance` before any row or POST when the wallet provably can't cover it; an unreadable balance never blocks — the POST is the authority.
160
160
 
161
+ ## Issuance
162
+
163
+ `Solrengine::Sdp::Token` issues a fungible SPL token through SDP and records every supply action as an engine-owned audit row. Mint authority is a custodial signing wallet (`signing_wallet_id`).
164
+
165
+ ```ruby
166
+ token = Solrengine::Sdp::Token.register!( # off-chain record (create_token)
167
+ name: "Kudos Points", symbol: "KUDO", decimals: 0,
168
+ signing_wallet_id: treasury_wallet_id
169
+ )
170
+ token.deploy! # on-chain mint (deploy_token)
171
+
172
+ token.mint!(destination: user.wallet_address, amount: 10) # credit — async, MintJob
173
+ token.burn!(source: user.wallet_address, # debit — async, BurnJob
174
+ signing_wallet_id: user.sdp_wallet_id, amount: 10)
175
+ ```
176
+
177
+ `amount` is a **decimal token amount** — SDP scales it by the token's `decimals`, so `10` on a 0-decimal token is 10 whole tokens (it is **not** base units). Whole amounts are sent without a trailing `.0` (which SDP rejects for a 0-decimal token as "too many decimal places").
178
+
179
+ Each `mint!`/`burn!` persists a `TokenMint`/`TokenBurn` row, then enqueues a single **never-retried** POST. The atomic claim (`minting → in_flight`) guarantees a mint/burn is never sent twice (SDP has no idempotency key); unlike a transfer there is no mint-transaction list to reconcile against, so a read-timeout lands `unknown` and is surfaced, never auto-re-sent.
180
+
181
+ | Mint / Burn status | Terminal? | Meaning |
182
+ |---|---|---|
183
+ | `minting` / `burning` | No | Recorded, not yet sent. |
184
+ | `in_flight` | No | Claimed; the POST is in flight. A crash here is left for manual reconcile, never re-sent. |
185
+ | `minted` / `burned` | Yes | Confirmed on-chain. |
186
+ | `failed` | Yes | SDP rejected it, or it was never sent — reason on `sdp_error`. |
187
+ | `unknown` | No | POST read-timeout: outcome uncertain. Never re-sent; surfaced for review. |
188
+
189
+ App-initiated issuance is its own doorbell: on settle, `MintJob`/`BurnJob` ring the balance broadcast (the configured `broadcast_targets`) for the affected wallet, so an earn/redeem updates the UI live — including the first — without relying on the chain-WebSocket watcher, which sees a wallet's native account, not the token ATA a mint credits.
190
+
161
191
  ## Errors
162
192
 
163
193
  Engine errors (all `< Solrengine::Sdp::Error < StandardError`):
@@ -172,7 +202,7 @@ Transport and API errors raised while talking to SDP come from the client gem
172
202
 
173
203
  ## SDP compatibility
174
204
 
175
- Tested against SDP **v0.28** (`Solrengine::Sdp::COMPATIBLE_SDP_VERSION`). SDP is pre-1.0 and breaks its API between minors; the compatible version is bumped — and the suite re-verified — on every SDP upgrade rather than claiming an open-ended range.
205
+ Tested against SDP **v0.31** (`Solrengine::Sdp::COMPATIBLE_SDP_VERSION`). SDP is pre-1.0 and breaks its API between minors; the compatible version is bumped — and the suite re-verified — on every SDP upgrade rather than claiming an open-ended range.
176
206
 
177
207
  ## Local development
178
208
 
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solrengine
4
+ module Sdp
5
+ # Sends a single burn to SDP, off the web request. The burn POST is NEVER
6
+ # retried; the atomic claim (burning → in_flight) guarantees a burn is
7
+ # never sent twice. Counterpart to MintJob — see it for the full rationale.
8
+ class BurnJob < ActiveJob::Base
9
+ queue_as :default
10
+
11
+ discard_on ActiveJob::DeserializationError
12
+
13
+ def perform(burn)
14
+ return unless burn.claim! # false → already attempted; never re-send
15
+
16
+ burn.submit_to_sdp!
17
+ # See MintJob: the burn is its own doorbell, so ring the source
18
+ # wallet's balance broadcast directly when the burn settles.
19
+ broadcast_balance(burn.source) if burn.burned?
20
+ end
21
+
22
+ private
23
+
24
+ def broadcast_balance(wallet_address)
25
+ Broadcaster.call(wallet_address)
26
+ rescue StandardError => e
27
+ Rails.logger&.warn("[Solrengine::Sdp::BurnJob] post-burn broadcast failed: #{e.class}: #{e.message}")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solrengine
4
+ module Sdp
5
+ # Sends a single mint to SDP, off the web request. The mint POST is NEVER
6
+ # retried — SDP has no idempotency key, so a blind re-send risks a
7
+ # double-mint. The atomic claim (minting → in_flight) means a crash +
8
+ # queue-retry, or two workers racing the same row, can never send twice:
9
+ # only the claim winner POSTs; a row already claimed is left as-is.
10
+ #
11
+ # Transport failures are handled INSIDE TokenMint#submit_to_sdp!
12
+ # (Timeout → unknown, Unavailable → failed), so this job never raises for
13
+ # an automatic retry to catch.
14
+ #
15
+ # Inherits ActiveJob::Base directly so the engine never depends on the
16
+ # host app's ApplicationJob (same posture as ProvisionWalletJob).
17
+ class MintJob < ActiveJob::Base
18
+ queue_as :default
19
+
20
+ # Row deleted between enqueue and perform: nothing to mint.
21
+ discard_on ActiveJob::DeserializationError
22
+
23
+ def perform(mint)
24
+ return unless mint.claim! # false → already attempted; never re-send
25
+
26
+ mint.submit_to_sdp!
27
+ # App-initiated issuance is its own doorbell. The chain-WebSocket
28
+ # watcher only sees a wallet's native account, not the token ATA a
29
+ # mint credits, so it never rings for an earn — but WE know the mint
30
+ # just landed. Ring the balance broadcast directly (every earn,
31
+ # including the first). A broadcast failure must never fail a settled
32
+ # mint: the money already moved.
33
+ broadcast_balance(mint.destination) if mint.minted?
34
+ end
35
+
36
+ private
37
+
38
+ def broadcast_balance(wallet_address)
39
+ Broadcaster.call(wallet_address)
40
+ rescue StandardError => e
41
+ Rails.logger&.warn("[Solrengine::Sdp::MintJob] post-mint broadcast failed: #{e.class}: #{e.message}")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "securerandom"
5
+
6
+ module Solrengine
7
+ module Sdp
8
+ # Engine-owned record of a token issued through SDP: registered off-chain
9
+ # (create_token), then deployed on-chain (deploy_token), then minted/burned
10
+ # via supply actions. Mirrors Transfer's "the row is the audit trail"
11
+ # posture. Mint authority is signing_wallet_id (a custodial treasury wallet).
12
+ class Token < ActiveRecord::Base
13
+ self.table_name = "solrengine_sdp_tokens"
14
+
15
+ STATUSES = %w[created deployed failed].freeze
16
+
17
+ has_many :mints, class_name: "Solrengine::Sdp::TokenMint", dependent: :destroy
18
+ has_many :burns, class_name: "Solrengine::Sdp::TokenBurn", dependent: :destroy
19
+
20
+ validates :name, :symbol, :signing_wallet_id, presence: true
21
+ validates :decimals, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
22
+ validates :status, inclusion: { in: STATUSES }
23
+
24
+ def deployed?
25
+ mint_address.present?
26
+ end
27
+
28
+ class << self
29
+ # Registers the token with SDP (off-chain record) and persists it.
30
+ # Raises Sdp::Error if SDP rejects it (no row is created).
31
+ def register!(name:, symbol:, signing_wallet_id:, decimals: 0)
32
+ sdp = Solrengine::Sdp.client.create_token(
33
+ name: name, symbol: symbol, decimals: decimals, signing_wallet_id: signing_wallet_id
34
+ )
35
+ create!(
36
+ name: name, symbol: symbol, decimals: decimals,
37
+ signing_wallet_id: signing_wallet_id, sdp_token_id: sdp.id, status: "created"
38
+ )
39
+ end
40
+ end
41
+
42
+ # Custodial sign-and-send deploy → on-chain mint. Records the mint
43
+ # address; on failure the row is marked failed and the error re-raised
44
+ # for the caller to render. Never retried (money-path).
45
+ def deploy!
46
+ sdp = Solrengine::Sdp.client.deploy_token(sdp_token_id)
47
+ update!(mint_address: sdp.mint_address, status: "deployed")
48
+ self
49
+ rescue ::Sdp::Error => e
50
+ update!(status: "failed", sdp_error: e.message)
51
+ raise
52
+ end
53
+
54
+ # Records a mint and enqueues MintJob (the mint POST runs off the web
55
+ # request, never retried). amount is a DECIMAL token amount — SDP scales
56
+ # it by the token's decimals (whole numbers for a 0-decimal token).
57
+ # Returns the TokenMint row.
58
+ def mint!(destination:, amount:, memo: nil)
59
+ mint = mints.create!(
60
+ destination: destination,
61
+ amount: normalize_amount(amount),
62
+ memo: memo,
63
+ memo_token: "#{TokenMint::MEMO_TOKEN_PREFIX}#{SecureRandom.hex(8)}",
64
+ status: "minting",
65
+ submitted_at: Time.current
66
+ )
67
+ MintJob.perform_later(mint)
68
+ mint
69
+ end
70
+
71
+ # Records a burn and enqueues BurnJob (never retried). A burn is signed
72
+ # by the source wallet's owner, so signing_wallet_id is the holder's
73
+ # custodial wallet (the user), NOT the treasury. Returns the TokenBurn.
74
+ def burn!(source:, signing_wallet_id:, amount:, memo: nil)
75
+ burn = burns.create!(
76
+ source: source,
77
+ signing_wallet_id: signing_wallet_id,
78
+ amount: normalize_amount(amount),
79
+ memo: memo,
80
+ memo_token: "#{TokenBurn::MEMO_TOKEN_PREFIX}#{SecureRandom.hex(8)}",
81
+ status: "burning",
82
+ submitted_at: Time.current
83
+ )
84
+ BurnJob.perform_later(burn)
85
+ burn
86
+ end
87
+
88
+ private
89
+
90
+ # BigDecimal#to_s("F") always renders a trailing ".0" — so a whole "10"
91
+ # becomes "10.0", which a 0-decimal token refuses ("Amount has too many
92
+ # decimal places"). Drop trailing fractional zeros (and the now-bare
93
+ # decimal point) so whole amounts go out as "10". SDP still enforces the
94
+ # token's decimals, so an over-precise amount surfaces as a failed write.
95
+ def normalize_amount(value)
96
+ BigDecimal(value.to_s).to_s("F").sub(/\.?0+\z/, "")
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solrengine
4
+ module Sdp
5
+ # Engine-owned record of every burn attempted through SDP — the redeem-path
6
+ # counterpart to TokenMint, with the same never-double-* discipline.
7
+ #
8
+ # Status: burning → in_flight → burned | failed | unknown (see TokenMint
9
+ # for the rationale; a timed-out burn stays `unknown` — never re-sent, no
10
+ # listing to reconcile against).
11
+ #
12
+ # Unlike a mint (signed by the token's mint authority), a burn is signed by
13
+ # the SOURCE wallet's owner: signing_wallet_id is the user's custodial
14
+ # wallet, not the treasury.
15
+ class TokenBurn < ActiveRecord::Base
16
+ self.table_name = "solrengine_sdp_token_burns"
17
+
18
+ MEMO_TOKEN_PREFIX = "sdpburn-"
19
+ MEMO_SEPARATOR = " | "
20
+
21
+ STATUSES = %w[burning in_flight burned failed unknown].freeze
22
+ TERMINAL_STATUSES = %w[burned failed unknown].freeze
23
+
24
+ belongs_to :token, class_name: "Solrengine::Sdp::Token"
25
+
26
+ validates :source, :signing_wallet_id, :amount, :memo_token, presence: true
27
+ validates :status, inclusion: { in: STATUSES }
28
+
29
+ STATUSES.each { |s| define_method("#{s}?") { status == s } }
30
+
31
+ scope :unsettled, -> { where.not(status: TERMINAL_STATUSES) }
32
+
33
+ def pending?
34
+ %w[burning in_flight].include?(status)
35
+ end
36
+
37
+ # Atomic claim: burning → in_flight in one UPDATE, so a crash + retry or
38
+ # two workers can never send the same burn twice.
39
+ def claim!
40
+ won = self.class.where(id: id, status: "burning").update_all(status: "in_flight", updated_at: Time.current)
41
+ reload if won == 1
42
+ won == 1
43
+ end
44
+
45
+ # The single, never-retried burn POST.
46
+ def submit_to_sdp!
47
+ tx = Solrengine::Sdp.client.burn_token(
48
+ token.sdp_token_id,
49
+ signing_wallet_id: signing_wallet_id,
50
+ source: source,
51
+ amount: amount,
52
+ memo: composed_memo
53
+ )
54
+ update!(
55
+ status: terminal_for(tx.status),
56
+ signature: tx.signature,
57
+ sdp_transaction_id: tx.id,
58
+ sdp_error: tx.error,
59
+ settled_at: Time.current
60
+ )
61
+ rescue ::Sdp::Timeout
62
+ update!(status: "unknown")
63
+ rescue ::Sdp::Unavailable => e
64
+ update!(status: "failed", sdp_error: "unsent: #{e.message}", settled_at: Time.current)
65
+ rescue ::Sdp::Error => e
66
+ update!(status: "failed", sdp_error: e.message, settled_at: Time.current)
67
+ end
68
+
69
+ def composed_memo
70
+ [ memo, memo_token ].compact.join(MEMO_SEPARATOR)
71
+ end
72
+
73
+ private
74
+
75
+ def terminal_for(sdp_status)
76
+ case sdp_status.to_s
77
+ when "confirmed", "finalized" then "burned"
78
+ when "failed" then "failed"
79
+ else "unknown"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solrengine
4
+ module Sdp
5
+ # Engine-owned record of every mint attempted through SDP — the audit
6
+ # trail and the never-double-mint guard.
7
+ #
8
+ # Status machine:
9
+ # minting → just recorded, not yet sent (created before the POST)
10
+ # in_flight → claimed by MintJob; the POST is being sent (atomic claim
11
+ # so a crash + queue-retry never re-sends the same mint)
12
+ # minted → confirmed on-chain (terminal)
13
+ # failed → SDP rejected it, or it was never sent (terminal)
14
+ # unknown → the POST read-timed out: outcome uncertain. NEVER re-sent
15
+ # (double-mint risk), and SDP exposes no mint-transaction
16
+ # listing to reconcile against — surfaced for manual review.
17
+ #
18
+ # The mint POST is NEVER retried (SDP has no idempotency key). Unlike a
19
+ # Transfer there is no memo-token reconcile path (no list endpoint for
20
+ # mint transactions), so a timed-out mint stays `unknown` rather than
21
+ # auto-resolving.
22
+ class TokenMint < ActiveRecord::Base
23
+ self.table_name = "solrengine_sdp_token_mints"
24
+
25
+ MEMO_TOKEN_PREFIX = "sdpmint-"
26
+ MEMO_SEPARATOR = " | "
27
+
28
+ STATUSES = %w[minting in_flight minted failed unknown].freeze
29
+ TERMINAL_STATUSES = %w[minted failed unknown].freeze
30
+
31
+ belongs_to :token, class_name: "Solrengine::Sdp::Token"
32
+
33
+ validates :destination, :amount, :memo_token, presence: true
34
+ validates :status, inclusion: { in: STATUSES }
35
+
36
+ STATUSES.each { |s| define_method("#{s}?") { status == s } }
37
+
38
+ scope :unsettled, -> { where.not(status: TERMINAL_STATUSES) }
39
+
40
+ def pending?
41
+ %w[minting in_flight].include?(status)
42
+ end
43
+
44
+ # Atomic claim: minting → in_flight in one UPDATE. Returns true only for
45
+ # the caller that won the claim, so a crash + queue-retry (or two workers)
46
+ # can never send the same mint twice. A row stuck in_flight is a
47
+ # crashed-mid-send and is left for manual reconcile, never auto-re-sent.
48
+ def claim!
49
+ won = self.class.where(id: id, status: "minting").update_all(status: "in_flight", updated_at: Time.current)
50
+ reload if won == 1
51
+ won == 1
52
+ end
53
+
54
+ # The single, never-retried mint POST. Every outcome lands on the row.
55
+ def submit_to_sdp!
56
+ tx = Solrengine::Sdp.client.mint_token(
57
+ token.sdp_token_id,
58
+ signing_wallet_id: token.signing_wallet_id,
59
+ destination: destination,
60
+ amount: amount,
61
+ memo: composed_memo
62
+ )
63
+ update!(
64
+ status: terminal_for(tx.status),
65
+ signature: tx.signature,
66
+ sdp_transaction_id: tx.id,
67
+ token_account: tx.token_account,
68
+ sdp_error: tx.error,
69
+ settled_at: Time.current
70
+ )
71
+ rescue ::Sdp::Timeout
72
+ # Outcome unknown — NEVER re-send. No mint-tx listing to reconcile.
73
+ update!(status: "unknown")
74
+ rescue ::Sdp::Unavailable => e
75
+ # Request never processed — safe to mark failed (no money moved).
76
+ update!(status: "failed", sdp_error: "unsent: #{e.message}", settled_at: Time.current)
77
+ rescue ::Sdp::Error => e
78
+ update!(status: "failed", sdp_error: e.message, settled_at: Time.current)
79
+ end
80
+
81
+ def composed_memo
82
+ [ memo, memo_token ].compact.join(MEMO_SEPARATOR)
83
+ end
84
+
85
+ private
86
+
87
+ # mint_token returns a confirmed TokenTransaction on success. A non-
88
+ # confirmed status can't be polled (no get-mint endpoint), so anything
89
+ # that isn't clearly confirmed/finalized or failed lands as unknown.
90
+ def terminal_for(sdp_status)
91
+ case sdp_status.to_s
92
+ when "confirmed", "finalized" then "minted"
93
+ when "failed" then "failed"
94
+ else "unknown"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -21,7 +21,13 @@ module Solrengine
21
21
  class InstallGenerator < Rails::Generators::Base
22
22
  source_root File.expand_path("templates", __dir__)
23
23
 
24
- MIGRATIONS = %w[add_solrengine_sdp_to_users create_solrengine_sdp_transfers].freeze
24
+ MIGRATIONS = %w[
25
+ add_solrengine_sdp_to_users
26
+ create_solrengine_sdp_transfers
27
+ create_solrengine_sdp_tokens
28
+ create_solrengine_sdp_token_mints
29
+ create_solrengine_sdp_token_burns
30
+ ].freeze
25
31
 
26
32
  # The exact development block `rails new` emits — the only async config
27
33
  # this generator rewrites mechanically. Anything else async-but-custom
@@ -0,0 +1,25 @@
1
+ # Engine-owned record of every burn attempted through SDP — the audit trail
2
+ # and the never-double-burn guard. See Solrengine::Sdp::TokenBurn.
3
+ class CreateSolrengineSdpTokenBurns < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :solrengine_sdp_token_burns do |t|
6
+ t.references :token, null: false, foreign_key: { to_table: :solrengine_sdp_tokens }
7
+ t.string :source, null: false # wallet (pubkey) the tokens are burned from
8
+ t.string :signing_wallet_id, null: false # SDP wallet that signs (the source's owner)
9
+ t.string :amount, null: false # decimal token amount — never a float
10
+ t.string :memo
11
+ t.string :memo_token, null: false # engine reconcile token
12
+ t.string :status, null: false, default: "burning" # burning -> burned | failed | unknown
13
+ t.string :signature
14
+ t.string :sdp_transaction_id
15
+ t.string :sdp_error
16
+ t.datetime :submitted_at
17
+ t.datetime :settled_at
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :solrengine_sdp_token_burns, :memo_token, unique: true
23
+ add_index :solrengine_sdp_token_burns, :status
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # Engine-owned record of every mint attempted through SDP — the audit trail
2
+ # and the never-double-mint guard. See Solrengine::Sdp::TokenMint.
3
+ class CreateSolrengineSdpTokenMints < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :solrengine_sdp_token_mints do |t|
6
+ t.references :token, null: false, foreign_key: { to_table: :solrengine_sdp_tokens }
7
+ t.string :destination, null: false
8
+ t.string :amount, null: false # decimal token amount — never a float
9
+ t.string :memo # app memo (composes with memo_token)
10
+ t.string :memo_token, null: false # engine reconcile token
11
+ t.string :status, null: false, default: "minting" # minting -> minted | failed | unknown
12
+ t.string :signature
13
+ t.string :sdp_transaction_id
14
+ t.string :token_account
15
+ t.string :sdp_error
16
+ t.datetime :submitted_at
17
+ t.datetime :settled_at
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :solrengine_sdp_token_mints, :memo_token, unique: true
23
+ add_index :solrengine_sdp_token_mints, :status
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # Engine-owned record of a token issued through SDP — created off-chain, then
2
+ # deployed on-chain. See Solrengine::Sdp::Token for the lifecycle.
3
+ class CreateSolrengineSdpTokens < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :solrengine_sdp_tokens do |t|
6
+ t.string :sdp_token_id # SDP token record id (set on create)
7
+ t.string :mint_address # on-chain mint (set on deploy)
8
+ t.string :name, null: false
9
+ t.string :symbol, null: false
10
+ t.integer :decimals, null: false, default: 0
11
+ t.string :signing_wallet_id, null: false # mint authority (treasury)
12
+ t.string :status, null: false, default: "created" # created -> deployed -> failed
13
+ t.string :sdp_error
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :solrengine_sdp_tokens, :sdp_token_id, unique: true
19
+ end
20
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Solrengine
4
4
  module Sdp
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
 
7
7
  # The SDP release this engine version is tested against. SDP breaks its
8
8
  # API between minors; bump this (and re-verify) on every SDP upgrade.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solrengine-sdp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Ferrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
11
+ date: 2026-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -63,12 +63,20 @@ extra_rdoc_files: []
63
63
  files:
64
64
  - LICENSE
65
65
  - README.md
66
+ - app/jobs/solrengine/sdp/burn_job.rb
67
+ - app/jobs/solrengine/sdp/mint_job.rb
66
68
  - app/jobs/solrengine/sdp/provision_wallet_job.rb
67
69
  - app/jobs/solrengine/sdp/track_transfer_job.rb
70
+ - app/models/solrengine/sdp/token.rb
71
+ - app/models/solrengine/sdp/token_burn.rb
72
+ - app/models/solrengine/sdp/token_mint.rb
68
73
  - app/models/solrengine/sdp/transfer.rb
69
74
  - lib/generators/solrengine/sdp/install_generator.rb
70
75
  - lib/generators/solrengine/sdp/templates/add_solrengine_sdp_to_users.rb
71
76
  - lib/generators/solrengine/sdp/templates/cable.yml
77
+ - lib/generators/solrengine/sdp/templates/create_solrengine_sdp_token_burns.rb
78
+ - lib/generators/solrengine/sdp/templates/create_solrengine_sdp_token_mints.rb
79
+ - lib/generators/solrengine/sdp/templates/create_solrengine_sdp_tokens.rb
72
80
  - lib/generators/solrengine/sdp/templates/create_solrengine_sdp_transfers.rb
73
81
  - lib/generators/solrengine/sdp/templates/initializer.rb
74
82
  - lib/generators/solrengine/sdp/templates/sdp_watcher