-
Notifications
You must be signed in to change notification settings - Fork 214
3.0.0: Improve object encoding and decoding #862
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
Conversation
|
Hey @jasonpaulos, some of these look not to be backward compatible and to some extent breaking changes. Are they? |
|
@emg110 this is still a work in progress, but yes, there are breaking change here. That's why this is going into the v3 branch |
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.
As a reminder, this file is generated, and its changes are a result of algorand/generator#73
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.
As a reminder, this file is generated, and its changes are a result of algorand/generator#73
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.
These changes are mostly to move code which relies on SignedTransaction (i.e. code which produces signed msig txns) to src/multisigSigning.ts.
Otherwise there's be a circular dependency, because src/signedTransaction.ts depends on this file
| import { LogicSig, LogicSigAccount } from './logicsig.js'; | ||
| import { addressFromMultisigPreImg } from './multisig.js'; | ||
|
|
||
| function signLogicSigTransactionWithAddress( |
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.
Not positive this is covered by testing (pre-existing).
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.
appears to be covered as signLogicSigTransaction -> signLogicSigTransactionObject -> signLogicSigTransactionObject but not sure if all cases are covered
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.
This is an internal function, so it's not tested directly. But as Pavel pointed out, since every call to signLogicSigTransaction and signLogicSigTransactionObject resolves to this, and those are covered well here, this should have good coverage.
| if (!(data instanceof Map)) { | ||
| throw new Error(`Invalid decoded SignedTransaction: ${data}`); | ||
| } | ||
| return new SignedTransaction({ |
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.
should there be a check for more than one signature on decode?
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.
I'm not so sure about this. If you try to create a signed txn with more than one signature, then yes we should error and stop you because you're doing something wrong and it's best to fail early.
But if you're just trying to read such a transaction that someone else created, I don't see a clear benefit to erroring in that case. There are almost an unlimited number of ways you can invalidate a transaction, and I'm not sure it's worth the effort to try to validate every field completely upon decoding (we do validate that field types are correct, but field values/consistency is another thing entirely). That will be done when you send the transaction to a node for processing anyway.
gmalouf
left a comment
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.
Left a few asks, overall makes sense and is definitely cleaner/seemingly more sustainable. Some concern about exported functions not covered by tests (even before these changes).
| // eslint-disable-next-line class-methods-use-this | ||
| prepare(body: Record<string, any>): AccountApplicationResponse { | ||
| return AccountApplicationResponse.from_obj_for_encoding(body); | ||
| return AccountApplicationResponse.fromEncodingData( |
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.
imo this interface from user point of view is too verbose. Can't fromEncodingData check if it is getting raw json or a encodingSchema-compatible dict ?
This pattern repeats over and over and over except few places where dicts supplied into fromEncodingData.
Well, unless all these verboseness is only in auto-generated and not exposed to end user.
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.
Yes this is pretty verbose and not very nice.
When decoding from a serialized JSON string, decodeJSON can be used, e.g. decodeJSON(str, AccountApplicationResponse). This is what I expect users will need the vast majority of the time, not the verbose code present in the request classes.
In these request classes, we are in a bit of weird situation when prepare function is called, since the JSON response is already parsed into an object. I have a desire to change this so that the JSON string is the input to prepare, then decodeJSON can be used and the code would be cleaner. But this PR has already grown large, so I didn't want to attempt that now.
| await Promise.all(acctPromises); | ||
|
|
||
| return new DryrunRequest({ | ||
| txns: txns.map((st) => ({ ...st, txn: st.txn.get_obj_for_encoding() })), |
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.
Am I right and the generated code for DryrunRequest serialization on sending expects it (and all its props) to recursive Encodable ?
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.
Not really, that's more how the old code work, where get_obj_for_encoding was implemented generically by BaseModel and it attempted to call get_obj_for_encoding on all field values. See this file from develop: https://github.com/algorand/js-algorand-sdk/blob/981905ad8511cea706fc77d0c133bb1962f76927/src/client/v2/basemodel.ts
With the changes in this PR, it's more like DryrunRequest knows which of its fields are Encodable and it will call toEncodingData on them when its own toEncodingData method is called. There is no generic searching all objects for the magic function name.
| } | ||
|
|
||
| public isDefaultValue(data: unknown): boolean { | ||
| return data === false; |
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.
maybe undefined us also good enough default boolean (so that omitted from encoding) ?
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.
NamedMapSchema is where omitting empty values is implemented, and it works without needing to allow undefined here.
Separately, I recently introduced an OptionalSchema that you can wrap around any other schema to allow undefined values. It doesn't change how maps omit empty values, but it does allow for values to be decoded as undefined, which is useful in a language like JS without any native notion of zero values.
| ['gh', header.gh], | ||
| ['prev', header.prev], | ||
| ['proto', header.proto], | ||
| ['rate', header.rate], |
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.
missing TxnCounter uint64 codec:"tc" missing `RewardsLevel uint64 `codec:"earn" from rewardState, not sure what is the rule here.
also missing partupdrmv and all UpgradeState fields except proto
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.
Yes this block header representation is very outdated and wrong. We have an issue to fix in #846, and since this PR is large I didn't want to address that now. I only preserved the existing structure, even though it needs improvement
| import { LogicSig, LogicSigAccount } from './logicsig.js'; | ||
| import { addressFromMultisigPreImg } from './multisig.js'; | ||
|
|
||
| function signLogicSigTransactionWithAddress( |
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.
appears to be covered as signLogicSigTransaction -> signLogicSigTransactionObject -> signLogicSigTransactionObject but not sure if all cases are covered
src/signedTransaction.ts
Outdated
| ['sgnr', this.sgnr], | ||
| ]); | ||
| if (this.msig) { | ||
| data.set('msig', encodedMultiSigToEncodingData(this.msig)); |
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.
wouldn't it retain the empty sig property if msig provided?
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.
If msig is defined then sig must be undefined. Undefined values are dropped during msgpack and JSON preparation, so it makes no difference.
A previous version of the code looked something like:
if (this.sig) {
data.set('sig', this.sig)
}
if (this.msig) {
data.set('msig', encodedMultiSigToEncodingData(this.msig))
}Due to dropping undefined values this is equivalent to the current code.
| if (!keyExist) { | ||
| throw new Error(MULTISIG_KEY_NOT_EXIST_ERROR_MSG); | ||
| } | ||
|
|
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.
this misses msig check compare to the original code
const msigAddr = addressFromMultisigPreImg({
version,
threshold,
pks,
});
const senderAddr = signedTxn.txn.snd
? new Address(signedTxn.txn.snd)
: Address.zeroAddress();
if (!senderAddr.equals(msigAddr)) {
signedTxn.sgnr = msigAddr.publicKey;
}
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.
Yes, I removed this from createMultisigTransactionWithSignature because it's already checked in createMultisigTransaction, which is called at the beginning of the function.
createMultisigTransaction:
js-algorand-sdk/src/multisigSigning.ts
Lines 54 to 64 in b864c24
| // if the address of this multisig is different from the transaction sender, | |
| // we need to add the auth-addr field | |
| const msigAddr = addressFromMultisigPreImg({ | |
| version, | |
| threshold, | |
| pks, | |
| }); | |
| let sgnr: Address | undefined; | |
| if (!txn.sender.equals(msigAddr)) { | |
| sgnr = msigAddr; | |
| } |
algorand-msgpacklibraryfrom_obj_for_encodingandget_obj_for_encodingmethods. In their place, introduce the following:Encodableinterface with atoEncodingDatamethod, and a staticfromEncodingDatamethod. "Encoding data" is an intermediate representation of the object in a JSMap, similar to the output of our Python SDK'sdictifymethod.Encodableinterface also has agetEncodingSchemamethod and a staticencodingSchemaproperty, which return instances of the newSchemabase class.Schemaclass is how "encoding data" gets transformed for encoding/decoding to/from JSON and msgpack. For example,AddressSchemaconverts anAddressclass to its 32-byte public key for encoding in msgpack, and for JSON it converts the address to its base32 string form. Specifically these translations happen in theprepareMsgpack/fromPreparedMsgpackandprepareJSON/fromPreparedJSONmethods.EncodedSignedTransactioninterface withSignedTransactionclassWhile the introduction of
Schemaadds some complexity, it also has these benefits:toEncodingDataandfromEncodingDatacan be more straightforward translations now.NamedMapSchemaclass determines if empty values should be omitted and seamlessly restores omitted values when decoding.omitEmptywhich, if true, omits default values during encoding.OptionalSchemawhich can wrap another schema. It will restore omitted values toundefined.This relies on generator changes as well from algorand/generator#73