-
Notifications
You must be signed in to change notification settings - Fork 206
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
3.0.0: Improve object encoding and decoding #862
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 |
src/client/v2/algod/models/types.ts
Outdated
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
src/multisig.ts
Outdated
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.
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).
@@ -24,6 +24,8 @@ export default class AccountApplicationInformation extends JSONRequest< | |||
|
|||
// 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.
@@ -139,7 +139,7 @@ export async function createDryrun({ | |||
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-msgpack
libraryfrom_obj_for_encoding
andget_obj_for_encoding
methods. In their place, introduce the following:Encodable
interface with atoEncodingData
method, and a staticfromEncodingData
method. "Encoding data" is an intermediate representation of the object in a JSMap
, similar to the output of our Python SDK'sdictify
method.Encodable
interface also has agetEncodingSchema
method and a staticencodingSchema
property, which return instances of the newSchema
base class.Schema
class is how "encoding data" gets transformed for encoding/decoding to/from JSON and msgpack. For example,AddressSchema
converts anAddress
class 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
/fromPreparedMsgpack
andprepareJSON
/fromPreparedJSON
methods.EncodedSignedTransaction
interface withSignedTransaction
classWhile the introduction of
Schema
adds some complexity, it also has these benefits:toEncodingData
andfromEncodingData
can be more straightforward translations now.NamedMapSchema
class determines if empty values should be omitted and seamlessly restores omitted values when decoding.omitEmpty
which, if true, omits default values during encoding.OptionalSchema
which can wrap another schema. It will restore omitted values toundefined
.This relies on generator changes as well from algorand/generator#73