solace-squads-smart-accounts 0.1.0 → 0.1.1
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 +4 -4
- data/lib/solace/squads_smart_accounts/codecs_extensions.rb +8 -0
- data/lib/solace/squads_smart_accounts/composers/create_smart_account_composer.rb +16 -7
- data/lib/solace/squads_smart_accounts/instructions/create_smart_account_instruction.rb +7 -3
- data/lib/solace/squads_smart_accounts/programs/squads_smart_account.rb +103 -3
- data/lib/solace/squads_smart_accounts/types/create_smart_account_event.rb +39 -0
- data/lib/solace/squads_smart_accounts/types/log_event_args_v2.rb +40 -0
- data/lib/solace/squads_smart_accounts/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e2d91139fbc5fb9289950a43d758100543f42bb0e3ad2dd537279e8c65020ba
|
|
4
|
+
data.tar.gz: df7ec92b6b9f92708397131d871cb03367bab0121e9bec4e7d228319367703e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46612e52fe84c894caee64a5fdcc037584c2fee6ba18621bd67cb211db1b78726053159b729338d69625dce186025d879bdd2d56b061538d4815441ff5ce5ed6
|
|
7
|
+
data.tar.gz: bbaaf0afb4bfa57582cd834ecfd6276366da02b5b73f41502d2a575504714fed1103211327d3ea1b402e617d41ee8a8ce2ba6d6ea5426e5ffb7596a1a51de9f2
|
|
@@ -189,6 +189,14 @@ module Solace
|
|
|
189
189
|
Array.new(decode_le_u32(stream)) { decode_pubkey(stream) }
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
+
# Decodes a Borsh bytes / Vec<u8> field: u32 LE length prefix + raw bytes.
|
|
193
|
+
#
|
|
194
|
+
# @param stream [IO, StringIO] The stream to read from.
|
|
195
|
+
# @return [String] The raw bytes as a binary string.
|
|
196
|
+
def decode_bytes(stream)
|
|
197
|
+
stream.read(decode_le_u32(stream))
|
|
198
|
+
end
|
|
199
|
+
|
|
192
200
|
# Decodes a u8 from 1 byte.
|
|
193
201
|
#
|
|
194
202
|
# @param stream [IO, StringIO] The stream to read from.
|
|
@@ -7,8 +7,12 @@ module Solace
|
|
|
7
7
|
# Required params:
|
|
8
8
|
# :creator [String] Base58 pubkey of the account creating the smart account.
|
|
9
9
|
# :treasury [String] Base58 pubkey of the treasury (from ProgramConfig).
|
|
10
|
-
# :settings [
|
|
11
|
-
#
|
|
10
|
+
# :settings [#to_s, Array<#to_s>] The settings PDA(s) to offer as remaining accounts.
|
|
11
|
+
# Pass a single address for deterministic creation
|
|
12
|
+
# (derive via Programs::SquadsSmartAccount.get_settings_address),
|
|
13
|
+
# or an array (a "window" of candidates) for race-free creation —
|
|
14
|
+
# the program initializes whichever matches the incremented
|
|
15
|
+
# counter. See Programs::SquadsSmartAccount.next_smart_account_candidates.
|
|
12
16
|
# :threshold [Integer] Number of approvals required to execute a transaction.
|
|
13
17
|
# :signers [Array<SquadsSmartAccounts::SmartAccountSigner>] Signers on the smart account.
|
|
14
18
|
# :time_lock [Integer] Seconds between proposal and execution (0 to disable).
|
|
@@ -32,11 +36,14 @@ module Solace
|
|
|
32
36
|
params[:creator].to_s
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
#
|
|
39
|
+
# Normalizes the :settings param to an array of base58 addresses.
|
|
36
40
|
#
|
|
37
|
-
#
|
|
41
|
+
# Accepts a single pubkey (deterministic creation) or an array of candidate
|
|
42
|
+
# pubkeys (windowed, race-free creation), so both flows share this composer.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<String>] The settings address(es), in seed order.
|
|
38
45
|
def settings
|
|
39
|
-
params[:settings].to_s
|
|
46
|
+
Array(params[:settings]).map(&:to_s)
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
# Returns the program config address from the constants
|
|
@@ -111,7 +118,9 @@ module Solace
|
|
|
111
118
|
# Writable accounts
|
|
112
119
|
account_context.add_writable_nonsigner(config)
|
|
113
120
|
account_context.add_writable_nonsigner(treasury)
|
|
114
|
-
|
|
121
|
+
|
|
122
|
+
# Each candidate settings PDA is offered as a writable remaining account.
|
|
123
|
+
settings.each { |address| account_context.add_writable_nonsigner(address) }
|
|
115
124
|
|
|
116
125
|
# Writable signers
|
|
117
126
|
account_context.add_writable_signer(creator)
|
|
@@ -134,7 +143,7 @@ module Solace
|
|
|
134
143
|
creator_index: context.index_of(creator),
|
|
135
144
|
system_program_index: context.index_of(system_program),
|
|
136
145
|
program_index: context.index_of(program_id),
|
|
137
|
-
settings_index: context.index_of(
|
|
146
|
+
settings_index: settings.map { |address| context.index_of(address) }
|
|
138
147
|
)
|
|
139
148
|
end
|
|
140
149
|
end
|
|
@@ -14,7 +14,9 @@ module Solace
|
|
|
14
14
|
# 2. creator — writable, signer
|
|
15
15
|
# 3. systemProgram — readonly, non-signer
|
|
16
16
|
# 4. program — readonly, non-signer
|
|
17
|
-
# 5
|
|
17
|
+
# 5+. settings — writable, non-signer (one or more candidate settings
|
|
18
|
+
# PDAs as remaining accounts; the program initializes
|
|
19
|
+
# the one matching the freshly incremented counter)
|
|
18
20
|
class CreateSmartAccountInstruction
|
|
19
21
|
# 8-byte Anchor discriminator: SHA256("global:create_smart_account")[0..7]
|
|
20
22
|
DISCRIMINATOR = [197, 102, 253, 231, 77, 84, 50, 17].freeze
|
|
@@ -33,7 +35,9 @@ module Solace
|
|
|
33
35
|
# @param creator_index [Integer] Account index of creator.
|
|
34
36
|
# @param system_program_index [Integer] Account index of systemProgram.
|
|
35
37
|
# @param program_index [Integer] Account index of the Squads program.
|
|
36
|
-
# @param settings_index [Integer] Account index of the
|
|
38
|
+
# @param settings_index [Integer, Array<Integer>] Account index/indices of the
|
|
39
|
+
# candidate settings PDA(s), appended as remaining accounts. Pass a single
|
|
40
|
+
# index for deterministic creation, or an array (a window) for race-free creation.
|
|
37
41
|
# @return [Solace::Instruction]
|
|
38
42
|
def self.build(
|
|
39
43
|
settings_authority:,
|
|
@@ -57,7 +61,7 @@ module Solace
|
|
|
57
61
|
creator_index,
|
|
58
62
|
system_program_index,
|
|
59
63
|
program_index,
|
|
60
|
-
settings_index
|
|
64
|
+
*Array(settings_index)
|
|
61
65
|
]
|
|
62
66
|
|
|
63
67
|
ix.data = data(
|
|
@@ -17,6 +17,13 @@ module Solace
|
|
|
17
17
|
#
|
|
18
18
|
# @see Solace::SquadsSmartAccounts
|
|
19
19
|
class SquadsSmartAccount < Base
|
|
20
|
+
# Default number of candidate settings PDAs offered by windowed creation.
|
|
21
|
+
# The on-chain counter must land inside this window for the transaction to
|
|
22
|
+
# succeed; ~20 absorbs heavy concurrency while staying well under the
|
|
23
|
+
# transaction account limit. Non-winning candidates are never initialized,
|
|
24
|
+
# so a larger window costs transaction size only, never rent.
|
|
25
|
+
DEFAULT_CREATION_WINDOW = 20
|
|
26
|
+
|
|
20
27
|
class << self
|
|
21
28
|
# Gets the address of the settings PDA for a given settings seed.
|
|
22
29
|
#
|
|
@@ -229,6 +236,36 @@ module Solace
|
|
|
229
236
|
)
|
|
230
237
|
end
|
|
231
238
|
|
|
239
|
+
# Fetches a confirmed createSmartAccount transaction and deserializes the
|
|
240
|
+
# CreateSmartAccountEvent it emitted, revealing the settings address the
|
|
241
|
+
# program actually created.
|
|
242
|
+
#
|
|
243
|
+
# This is how a windowed creation (see {#compose_create_smart_account} with
|
|
244
|
+
# `window > 1`) learns which candidate won: the program picks one of the
|
|
245
|
+
# offered PDAs, observable only after the transaction lands. Match the
|
|
246
|
+
# returned `new_settings_pubkey` against your {#next_smart_account_candidates}
|
|
247
|
+
# to recover the seed and vault.
|
|
248
|
+
#
|
|
249
|
+
# @param signature [String] Signature of the confirmed createSmartAccount transaction.
|
|
250
|
+
# @return [SquadsSmartAccounts::CreateSmartAccountEvent] The deserialized event.
|
|
251
|
+
# @raise [RuntimeError] If the transaction is missing or carries no logEvent.
|
|
252
|
+
def get_created_smart_account_event(signature:)
|
|
253
|
+
transaction = connection.get_transaction(
|
|
254
|
+
signature,
|
|
255
|
+
commitment: 'confirmed',
|
|
256
|
+
encoding: 'json',
|
|
257
|
+
maxSupportedTransactionVersion: 0
|
|
258
|
+
)
|
|
259
|
+
raise "Transaction not found for signature #{signature}" unless transaction
|
|
260
|
+
|
|
261
|
+
io = log_event_stream(transaction)
|
|
262
|
+
raise "No logEvent inner instruction found in transaction #{signature}" unless io
|
|
263
|
+
|
|
264
|
+
args = Solace::SquadsSmartAccounts::LogEventArgsV2.deserialize(io)
|
|
265
|
+
|
|
266
|
+
Solace::SquadsSmartAccounts::CreateSmartAccountEvent.deserialize(StringIO.new(args.event))
|
|
267
|
+
end
|
|
268
|
+
|
|
232
269
|
# Gets the full deterministic identity of the next smart account to be
|
|
233
270
|
# created: the settings seed, settings address, and default vault address.
|
|
234
271
|
#
|
|
@@ -252,6 +289,33 @@ module Solace
|
|
|
252
289
|
)
|
|
253
290
|
end
|
|
254
291
|
|
|
292
|
+
# Gets a window of candidate identities for race-free creation: the next
|
|
293
|
+
# `count` consecutive smart accounts (seeds `index+1 .. index+count`).
|
|
294
|
+
#
|
|
295
|
+
# Pass the candidates' settings addresses to {#create_smart_account_windowed},
|
|
296
|
+
# which offers them all and lets the program pick whichever matches the
|
|
297
|
+
# freshly incremented counter — so creation succeeds even if other accounts
|
|
298
|
+
# are created concurrently, as long as the true index lands in the window.
|
|
299
|
+
#
|
|
300
|
+
# @param count [Integer] Size of the candidate window (default: {DEFAULT_CREATION_WINDOW}).
|
|
301
|
+
# @return [Array<SquadsSmartAccounts::SmartAccountIdentity>] Candidate identities, in seed order.
|
|
302
|
+
def next_smart_account_candidates(count: DEFAULT_CREATION_WINDOW)
|
|
303
|
+
start_seed = get_program_config.smart_account_index + 1
|
|
304
|
+
|
|
305
|
+
Array.new(count) do |offset|
|
|
306
|
+
settings_seed = start_seed + offset
|
|
307
|
+
|
|
308
|
+
settings_address, = get_settings_address(settings_seed:)
|
|
309
|
+
smart_account_address, = get_smart_account_address(settings_address:)
|
|
310
|
+
|
|
311
|
+
Solace::SquadsSmartAccounts::SmartAccountIdentity.new(
|
|
312
|
+
settings_seed:,
|
|
313
|
+
settings_address:,
|
|
314
|
+
smart_account_address:
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
255
319
|
# Creates a new smart account, signs it, and (optionally) sends it.
|
|
256
320
|
#
|
|
257
321
|
# @example Create a smart account, retaining the values to index
|
|
@@ -300,10 +364,18 @@ module Solace
|
|
|
300
364
|
# clients can derive and persist the settings and vault addresses before
|
|
301
365
|
# sending the transaction.
|
|
302
366
|
#
|
|
303
|
-
#
|
|
367
|
+
# With the default `window: 1` this offers a single settings PDA and is
|
|
368
|
+
# subject to the same races as {#next_smart_account}. Pass `window > 1` to
|
|
369
|
+
# offer a window of consecutive candidate PDAs (seeds `settings_seed ..
|
|
370
|
+
# settings_seed + window - 1`); the program initializes whichever matches the
|
|
371
|
+
# freshly incremented counter, so creation tolerates concurrent creations.
|
|
372
|
+
# The chosen address is then resolved via {#get_created_smart_account_event}.
|
|
373
|
+
#
|
|
374
|
+
# @param settings_seed [Integer] The (starting) seed for the settings PDA (see {#next_settings_seed}).
|
|
304
375
|
# @param creator [#to_s, Keypair] The account creating the smart account (must sign).
|
|
305
376
|
# @param threshold [Integer] Number of approvals required to execute a transaction.
|
|
306
377
|
# @param signers [Array<SquadsSmartAccounts::SmartAccountSigner>] Signers on the smart account.
|
|
378
|
+
# @param window [Integer] (Optional) Number of candidate PDAs to offer (default: 1).
|
|
307
379
|
# @param time_lock [Integer] (Optional) Seconds between proposal and execution (default: 0).
|
|
308
380
|
# @param settings_authority [#to_s] (Optional) Pubkey of the reconfiguration authority.
|
|
309
381
|
# @param rent_collector [#to_s] (Optional) Pubkey for reclaiming rent on closed accounts.
|
|
@@ -314,6 +386,7 @@ module Solace
|
|
|
314
386
|
creator:,
|
|
315
387
|
threshold:,
|
|
316
388
|
signers:,
|
|
389
|
+
window: 1,
|
|
317
390
|
time_lock: 0,
|
|
318
391
|
settings_authority: nil,
|
|
319
392
|
rent_collector: nil,
|
|
@@ -321,12 +394,15 @@ module Solace
|
|
|
321
394
|
)
|
|
322
395
|
program_config = get_program_config
|
|
323
396
|
|
|
324
|
-
|
|
397
|
+
settings = Array.new(window) do |offset|
|
|
398
|
+
address, = get_settings_address(settings_seed: settings_seed + offset)
|
|
399
|
+
address
|
|
400
|
+
end
|
|
325
401
|
|
|
326
402
|
create_smart_account_ix = Composers::SquadsSmartAccountsCreateSmartAccountComposer.new(
|
|
327
403
|
creator:,
|
|
328
404
|
treasury: program_config.treasury,
|
|
329
|
-
settings
|
|
405
|
+
settings:,
|
|
330
406
|
threshold:,
|
|
331
407
|
signers:,
|
|
332
408
|
time_lock:,
|
|
@@ -1813,6 +1889,30 @@ module Solace
|
|
|
1813
1889
|
|
|
1814
1890
|
private
|
|
1815
1891
|
|
|
1892
|
+
# Locates the program's `logEvent` self-CPI among a landed transaction's
|
|
1893
|
+
# inner instructions and returns its args as a bytestream (the instruction
|
|
1894
|
+
# data past the 8-byte discriminator), or nil if absent. The instruction is
|
|
1895
|
+
# identified by its stable discriminator, so the match is exact.
|
|
1896
|
+
#
|
|
1897
|
+
# @param transaction [Hash] The raw getTransaction result.
|
|
1898
|
+
# @return [StringIO, nil] Stream positioned at the start of the LogEventArgsV2.
|
|
1899
|
+
def log_event_stream(transaction)
|
|
1900
|
+
groups = transaction.dig('meta', 'innerInstructions') || []
|
|
1901
|
+
|
|
1902
|
+
groups.each do |group|
|
|
1903
|
+
Array(group['instructions']).each do |ix|
|
|
1904
|
+
next if ix['data'].nil?
|
|
1905
|
+
|
|
1906
|
+
binary = Solace::Utils::Codecs.base58_to_binary(ix['data'])
|
|
1907
|
+
next unless binary.byteslice(0, 8).bytes == Solace::SquadsSmartAccounts::LogEventArgsV2::DISCRIMINATOR
|
|
1908
|
+
|
|
1909
|
+
return StringIO.new(binary.byteslice(8..))
|
|
1910
|
+
end
|
|
1911
|
+
end
|
|
1912
|
+
|
|
1913
|
+
nil
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1816
1916
|
# Derives the vault and destination ATAs for a token spending-limit spend.
|
|
1817
1917
|
#
|
|
1818
1918
|
# @param smart_account [#to_s] The vault PDA (owner of the source ATA).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module SquadsSmartAccounts
|
|
5
|
+
# Immutable value object representing the `CreateSmartAccountEvent` the program
|
|
6
|
+
# emits when a smart account is created. It is the `CreateSmartAccountEvent`
|
|
7
|
+
# variant of the Borsh `SmartAccountEvent` enum carried inside
|
|
8
|
+
# {LogEventArgs#event}, so deserialization reads the 1-byte enum variant tag
|
|
9
|
+
# before the event fields.
|
|
10
|
+
#
|
|
11
|
+
# On windowed creation the program picks one candidate settings PDA from the
|
|
12
|
+
# offered window; this event is the only way to learn which one was chosen
|
|
13
|
+
# (see Solace::Programs::SquadsSmartAccount#created_smart_account_settings).
|
|
14
|
+
#
|
|
15
|
+
# The on-chain event also carries `new_settings_content: Settings`, but only
|
|
16
|
+
# `new_settings_pubkey` is decoded here — it is all the caller needs to derive
|
|
17
|
+
# the vault, and the trailing Settings can be fetched on demand.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# event = CreateSmartAccountEvent.deserialize(StringIO.new(log_event_args.event))
|
|
21
|
+
# event.new_settings_pubkey # => base58 settings address
|
|
22
|
+
CreateSmartAccountEvent = Data.define(
|
|
23
|
+
:new_settings_pubkey # String — base58 address of the newly created settings account
|
|
24
|
+
) do
|
|
25
|
+
# Deserializes a CreateSmartAccountEvent from a stream of Borsh-encoded
|
|
26
|
+
# SmartAccountEvent bytes (positioned at the enum variant tag).
|
|
27
|
+
#
|
|
28
|
+
# @param io [IO, StringIO] Stream positioned at the start of the event.
|
|
29
|
+
# @return [CreateSmartAccountEvent] The deserialized, frozen event value.
|
|
30
|
+
def self.deserialize(io)
|
|
31
|
+
Solace::Utils::Codecs.decode_u8(io) # SmartAccountEvent enum variant tag
|
|
32
|
+
|
|
33
|
+
new(
|
|
34
|
+
new_settings_pubkey: Solace::Utils::Codecs.decode_pubkey(io)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module SquadsSmartAccounts
|
|
5
|
+
# Immutable value object for the arguments of the program's `logEvent`
|
|
6
|
+
# instruction, the self-CPI through which the program records events. The
|
|
7
|
+
# deployed program takes `LogEventArgsV2 { event: Vec<u8> }` — a single
|
|
8
|
+
# Borsh-encoded `SmartAccountEvent`. (An older `LogEventArgs` form carrying
|
|
9
|
+
# `account_seeds`/`bump` is dead code in the program and described by the
|
|
10
|
+
# bundled IDL, but is never emitted.)
|
|
11
|
+
#
|
|
12
|
+
# Extracting the args bytestream from a landed transaction is the Program
|
|
13
|
+
# layer's responsibility — see
|
|
14
|
+
# Solace::Programs::SquadsSmartAccount#get_created_smart_account_event.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# args = LogEventArgsV2.deserialize(io)
|
|
18
|
+
# event = CreateSmartAccountEvent.deserialize(StringIO.new(args.event))
|
|
19
|
+
LogEventArgsV2 = Data.define(
|
|
20
|
+
:event # String — Borsh-encoded SmartAccountEvent bytes (binary string)
|
|
21
|
+
) do
|
|
22
|
+
# Deserializes a LogEventArgsV2 from a stream of Borsh-encoded instruction
|
|
23
|
+
# data (positioned just past the 8-byte logEvent discriminator).
|
|
24
|
+
#
|
|
25
|
+
# @param io [IO, StringIO] Stream positioned at the start of the args.
|
|
26
|
+
# @return [LogEventArgsV2] The deserialized, frozen args value.
|
|
27
|
+
def self.deserialize(io)
|
|
28
|
+
new(event: Solace::Utils::Codecs.decode_bytes(io))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 8-byte Anchor discriminator of the `logEvent` instruction (sha256("global:
|
|
33
|
+
# log_event")[0, 8]), a stable name-derived instruction discriminator — not
|
|
34
|
+
# per-event data. The Program layer uses it to locate the logEvent self-CPI
|
|
35
|
+
# among a transaction's inner instructions. (Defined here rather than inside
|
|
36
|
+
# the Data.define block, where constant assignment would leak to the enclosing
|
|
37
|
+
# module instead of the class.)
|
|
38
|
+
LogEventArgsV2::DISCRIMINATOR = [5, 9, 90, 141, 223, 134, 57, 217].freeze
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solace-squads-smart-accounts
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Scholl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -165,6 +165,8 @@ files:
|
|
|
165
165
|
- lib/solace/squads_smart_accounts/instructions/set_time_lock_as_authority_instruction.rb
|
|
166
166
|
- lib/solace/squads_smart_accounts/instructions/use_spending_limit_instruction.rb
|
|
167
167
|
- lib/solace/squads_smart_accounts/programs/squads_smart_account.rb
|
|
168
|
+
- lib/solace/squads_smart_accounts/types/create_smart_account_event.rb
|
|
169
|
+
- lib/solace/squads_smart_accounts/types/log_event_args_v2.rb
|
|
168
170
|
- lib/solace/squads_smart_accounts/types/period.rb
|
|
169
171
|
- lib/solace/squads_smart_accounts/types/permissions.rb
|
|
170
172
|
- lib/solace/squads_smart_accounts/types/program_config.rb
|