-
Notifications
You must be signed in to change notification settings - Fork 7
feat: scoped accounts #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c07d256
ff72060
f5a8ea6
95ccd08
a1f8795
71a2121
9435a4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,13 +54,19 @@ func (StatementFnCallResolution) fnCallResolution() {} | |
| func (r VarOriginFnCallResolution) GetParams() []string { return r.Params } | ||
| func (r StatementFnCallResolution) GetParams() []string { return r.Params } | ||
|
|
||
| const FnSetTxMeta = "set_tx_meta" | ||
| const FnSetAccountMeta = "set_account_meta" | ||
| const FnVarOriginMeta = "meta" | ||
| const FnVarOriginBalance = "balance" | ||
| const FnVarOriginOverdraft = "overdraft" | ||
| const FnVarOriginGetAsset = "get_asset" | ||
| const FnVarOriginGetAmount = "get_amount" | ||
| const ( | ||
| // Statemetn fns | ||
| FnSetTxMeta = "set_tx_meta" | ||
| FnSetAccountMeta = "set_account_meta" | ||
|
|
||
| // Expr fns | ||
| FnVarOriginMeta = "meta" | ||
| FnVarOriginBalance = "balance" | ||
| FnVarOriginOverdraft = "overdraft" | ||
| FnVarOriginGetAsset = "get_asset" | ||
| FnVarOriginGetAmount = "get_amount" | ||
| FnVarOriginScoped = "scoped" | ||
| ) | ||
|
|
||
| var Builtins = map[string]FnCallResolution{ | ||
| FnSetTxMeta: StatementFnCallResolution{ | ||
|
|
@@ -114,6 +120,17 @@ var Builtins = map[string]FnCallResolution{ | |
| }, | ||
| }, | ||
| }, | ||
| FnVarOriginScoped: VarOriginFnCallResolution{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 [major] Handle scoped() in involved-account analysis Adding |
||
| Params: []string{TypeAccount, TypeString}, | ||
| Return: TypeAccount, | ||
| Docs: "returns the scoped version of that account. Empty string means no scope. Overwrites the previous scope", | ||
| VersionConstraints: []VersionClause{ | ||
| { | ||
| Version: parser.NewVersionInterpreter(0, 0, 25), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 [blocker] Require the scoped feature flag for scoped() When a script enables only
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 [major] Require the scoped feature flag for scoped() When a script uses
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 [major] Use the scoped feature flag for scoped() For versioned scripts that call |
||
| FeatureFlag: flags.ExperimentalScopedFunction, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 [major] Use the scoped feature flag for scoped() For versioned programs using |
||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| type Diagnostic struct { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
|
|
||
| [TestPrettyPrintAccountsMetadata/without_scope_(no_Scope_column) - 1] | ||
| | [36mAccount[0m | [36mName[0m | [36mValue [0m | | ||
| | alice | kyc | verified | | ||
| | bob | tier | gold | | ||
| --- | ||
|
|
||
| [TestPrettyPrintAccountsMetadata/with_scope_(Scope_column_shown) - 1] | ||
| | [36mAccount[0m | [36mScope[0m | [36mName[0m | [36mValue [0m | | ||
| | alice | eu | kyc | pending | | ||
| | alice | | kyc | verified | | ||
| | bob | | tier | gold | | ||
| --- |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,53 +1,74 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package interpreter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "slices" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/formancehq/numscript/internal/utils" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountMetadata = map[string]string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountsMetadata map[string]AccountMetadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (m AccountsMetadata) fetchAccountMetadata(account string) AccountMetadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return utils.MapGetOrPutDefault(m, account, func() AccountMetadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AccountMetadata{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (m AccountsMetadata) DeepClone() AccountsMetadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cloned := make(AccountsMetadata) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for account, accountBalances := range m { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for asset, metadataValue := range accountBalances { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clonedAccountBalances := cloned.fetchAccountMetadata(account) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils.MapGetOrPutDefault(clonedAccountBalances, asset, func() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return metadataValue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cloned | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountMetadataRow struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Account string `json:"account"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Key string `json:"key"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Value string `json:"value"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Scope string `json:"scope,omitempty"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (m AccountsMetadata) Merge(update AccountsMetadata) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for acc, accBalances := range update { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cachedAcc := utils.MapGetOrPutDefault(m, acc, func() AccountMetadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AccountMetadata{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // AccountsMetadata is the external, serialized representation of account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // metadata. The runtime works with the in-memory InternalAccountsMetadata and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // converts to this at the boundaries (store queries, execution result). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountsMetadata []AccountMetadataRow | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for curr, amt := range accBalances { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cachedAcc[curr] = amt | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // FirstDuplicate returns the first row whose (account, key, scope) key already | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // appeared earlier in the list, if any. That triple is the identity of a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // metadata entry and the value is its content, so a repeated key is an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ambiguous, malformed input. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (rows AccountsMetadata) FirstDuplicate() (AccountMetadataRow, bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen := make(map[[3]string]struct{}, len(rows)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, row := range rows { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key := [3]string{row.Account, row.Key, row.Scope} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, ok := seen[key]; ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return row, true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen[key] = struct{}{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AccountMetadataRow{}, false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (m AccountsMetadata) PrettyPrint() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header := []string{"Account", "Name", "Value"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the Scope column is shown only when at least one entry has a scope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasScope := slices.ContainsFunc(m, func(row AccountMetadataRow) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return row.Scope != "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var header []string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hasScope { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = []string{"Account", "Scope", "Name", "Value"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = []string{"Account", "Name", "Value"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var rows [][]string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for account, accMetadata := range m { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for name, value := range accMetadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| row := []string{account, name, value} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows = append(rows, row) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, row := range m { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hasScope { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows = append(rows, []string{row.Account, row.Scope, row.Key, row.Value}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows = append(rows, []string{row.Account, row.Key, row.Value}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return utils.CsvPretty(header, rows, true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // CompareAccountsMetadata reports whether two metadata lists hold the same set | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // of rows, ignoring order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func CompareAccountsMetadata(a AccountsMetadata, b AccountsMetadata) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(a) != len(b) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, row := range a { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !slices.Contains(b, row) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix multiset comparison in This implementation can return true for unequal datasets when duplicates exist (e.g., Line 39 loop + Suggested fix-import (
- "slices"
-
- "github.com/formancehq/numscript/internal/utils"
-)
+import "github.com/formancehq/numscript/internal/utils"
@@
func CompareAccountsMetadata(a AccountsMetadata, b AccountsMetadata) bool {
if len(a) != len(b) {
return false
}
- for _, row := range a {
- if !slices.Contains(b, row) {
+ counts := make(map[AccountMetadataRow]int, len(a))
+ for _, row := range a {
+ counts[row]++
+ }
+ for _, row := range b {
+ if counts[row] == 0 {
return false
}
+ counts[row]--
}
return true
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package interpreter | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/gkampitakis/go-snaps/snaps" | ||
| ) | ||
|
|
||
| func TestPrettyPrintAccountsMetadata(t *testing.T) { | ||
| t.Run("without scope (no Scope column)", func(t *testing.T) { | ||
| meta := AccountsMetadata{ | ||
| {Account: "alice", Key: "kyc", Value: "verified"}, | ||
| {Account: "bob", Key: "tier", Value: "gold"}, | ||
| } | ||
|
|
||
| snaps.MatchSnapshot(t, meta.PrettyPrint()) | ||
| }) | ||
|
|
||
| t.Run("with scope (Scope column shown)", func(t *testing.T) { | ||
| meta := AccountsMetadata{ | ||
| {Account: "alice", Key: "kyc", Value: "verified"}, | ||
| {Account: "alice", Scope: "eu", Key: "kyc", Value: "pending"}, | ||
| {Account: "bob", Key: "tier", Value: "gold"}, | ||
| } | ||
|
|
||
| snaps.MatchSnapshot(t, meta.PrettyPrint()) | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package interpreter | ||
|
|
||
| import ( | ||
| "regexp" | ||
| ) | ||
|
|
||
| var scopeRegex = regexp.MustCompile(`^[a-z0-9_]*$`) | ||
|
|
||
| func validateScope(scope string) bool { | ||
| return scopeRegex.MatchString(scope) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package interpreter | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestAccountAddressString(t *testing.T) { | ||
| t.Run("no scope", func(t *testing.T) { | ||
| require.Equal(t, AccountAddress{Name: "acc"}.String(), "acc") | ||
| }) | ||
|
|
||
| t.Run("with scope", func(t *testing.T) { | ||
| require.Equal(t, AccountAddress{Name: "acc", Scope: "xyz"}.String(), "acc/xyz") | ||
| }) | ||
| } | ||
|
|
||
| func TestScopeValidation(t *testing.T) { | ||
| t.Run("valid scopes", func(t *testing.T) { | ||
| require.True(t, validateScope("")) | ||
| require.True(t, validateScope("myscope")) | ||
| require.True(t, validateScope("x")) | ||
| require.True(t, validateScope("x1")) | ||
| require.True(t, validateScope("my_scope_with_underscores")) | ||
| }) | ||
|
|
||
| t.Run("invalid scopes", func(t *testing.T) { | ||
| require.False(t, validateScope("!")) | ||
| require.False(t, validateScope("$")) | ||
| require.False(t, validateScope("UPPERCASE")) | ||
| require.False(t, validateScope("dash-case")) | ||
| require.False(t, validateScope("colons:within")) | ||
| }) | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 [major] Teach involved-account analysis about scoped()
Registering
scopedas an account-returning expression also needs support inGetInvolvedAccounts: scripts that usescoped(@src, "x")in a source, destination,balance, or metadata expression now reach the involved-account evaluator's function switch and fall through toUnboundFunctionErr. This breaks the publicParseResult.GetInvolvedAccountsAPI for the new feature.